Tuesday, June 15, 2010

Episode #99: The .needle in the /haystack

Tim is on the road:

This week I'm at the SANS Penetration Testing & Vulnerability Assessment Summit hanging out with Ed. And no, I don't get any money for saying that. Although, Ed did give me some money to stay away from him. Come to think of it, Hal did the same thing before. It must be that they just can't stand being next to the most handsome one of the trio, and it has nothing to do with my love of onions, garlic, and German Brick Cheese*.

Back in the regular world, I had a bunch of files to review and search, but I didn't have any idea what types of files were in the mix. I whipped up a quick some PowerShell to give me a quick overview of the file types in the directory tree. Once I knew what type of files I'm was dealing with, I was better able to pick the tool to review the documents. Here is the command:

PS C:\> ls mydir -Recurse | ? { -not $_.PSIsContainer } | group Extension -NoElement | sort count -desc

Count Name
----- ----
145 .pdf
19 .rtf
16 .doc
7 .xml
7 .docx
4 .xls
1
1 .xlsx
We start off by getting a recursive directory listing. The Where-Object cmdlet (alias ?) is used to remove directories from the listing. The PSIsContainer is a good way to differentiate files from folders, directories are containers and files aren't. Next, we then use Group-Object (alias group) to group based on file extension. The NoElement switch tells the Group-Object cmdlet not to include in the output the collection of all the file objects. Finally, we sort, in descending order, based on the count in each group. By the way, any parameter or switch name can be shortened as long as it is not ambiguous. We could use Des, but not D or De since it would match Descending and Debug.

I have to say, I have a bit of envy for the Linux "file" command. Although, since Windows relies so heavily on the file extension it typically works well unless someone is trying to hide something.

Let's see what Ed and Hal have cooking?

*Warning: Never try German Brick Cheese, it tastes like sewage smells. It makes Limburger smell like flowers. Seriously, don't try it. I bought some in college as a joke and we could smell it through the Ziploc back in the fridge. Bleh! Oh, and sorry to Eric and Kevin for tricking you into trying it.

Ed's On the Road Too
So, like, when Tim initially proposed this article, he was all like, “Yeah, just count the number of files of a given file suffix on a partition. This will be hard for Ed.” And, I was like, “Uh… Dude… as if. I mean, just totally do this:

C:\> dir /b /s C:\*.ini | find /c /v “”

And you’ll have the total number of ini files. Lather, rinse, and repeat for any other suffix, ya know.”

Tim responded, “Oh yeah. Never mind. I’ll write my part first.”

AND THEN… my esteemed colleague unfurls something that automatically figures out which extensions are on the partition and creates a beautiful summary of their counts. I’m not saying that he set me up. But, well, come to think of it, I think he set me up. It’s a good thing that I’m not very busy this week, or else that would have been a problem.

Well, Mr. Medin, if that is your real name, my German-brick-cheese-eating friend, put this in your cmd.exe pipe and smoke it:
C:\> cmd.exe /v:on /c "set directory=c:\windows\system32& (for /f "delims=" %i in 
('dir /a-D /L /s /b !directory!') do @set name=%i& echo !name:~-4,4! >>
c:\suffix.txt) & sort c:\suffix.txt > c:\sortsuf.txt & (set previous= & for /f
%j in (c:\sortsuf.txt) do @set current=%j& if NOT !current!==!previous! (echo
%j >> c:\uniqsuf.txt) & set previous=!current!) & for /f %k in (c:\uniqsuf.txt)
do @echo %k: & dir /b /s !directory!\*%k | find /c /v """ & del c:\suffix.txt
c:\sortsuf.txt c:\uniqsuf.txt
.acm:
10
.acs:
1
.bak:
2
.bat:
1
.bin:
4
.bmp:
1
.btr:
1
.bud:
2
---SNIP---
To make this whole shebang go, all ya have to do is put the appropriate directory in the “set directory =” part. Note that there is no space after the directory name and the &, which is important. When I first showed that command to Tim, he responded, “That, is art. Not-so-much Rembrandt as Salvador Dali.” You know, I’ve been considering growing one of those Dali-style mustaches.

As for the command itself, I think this is all pretty self-explanatory. Right? I mean, it just kinds rolls of the fingers and is the obvious way to do this. Easy as pie.

Well, if you insist, I’ll give you a synopsis of what’s going on here followed by the details. My command can be broken into three phases, along with a preamble up front and a clean-up action at the end. First, I isolate the last four characters of each file name (which should be the suffix, letting me catch stuff like “.xls” and “xlsx”), storing the result in a file called c:\suffix.txt. In the second phase, I jump into my uniquifier (virtually identical to the uniq command I implemented in Episode #91) which sorts the c:\suffix.txt file and plucks out the unique entries. And, thirdly, I then go through each of my unique suffixes and count the number of files that have each given suffix. There is a bit of a down side. If a file doesn’t have a three-or-four-character suffix, my command will give a “File Not Found” message, but that’s not so bad.

Those are the highlights of what I’ve wrought. Let’s jump into the details. In my preamble, I start by invoking delayed variable expansion (cmd.exe /v:on /c), because I’m gonna have a metric spit-ton of variables whose value will have to float as my command runs. Next, I set a variable called directory to the directory we’re going to count in. I didn’t have to do it this way, but without it, our dear user would have to type in the directory name a couple of different places. We care about human factors and ease of use here at CommandLineKungFuBlog. The directory name is immediately followed by an &, without a space. That way, it won’t pick up an extra space at the end in the variable value itself.

With that preliminary planning done, I move into Phase 1. I have a FOR /F loop, with default parsing on spaces and tabs turned off (“delims=”) and an iterator variable of %i. I’m iterating over the output of a dir command, with options set to not show directories (/a-D), with all file names in lower case (/L). I’ve gotta use the lowercase option here, or else we’d have separate counts for .doc, .DOC, .Doc, and so on. I want to recurse subdirectories (/s) and have the bare form of output (/b) so that I just get full file paths. And, of course, I want to do all of this for !directory!, using the !’s instead of %’s for the variable because I want the delayed expanded value of that sucker. In the body of my FOR loop, for each file, I take its name out of the iterator variable (%i) and stick it into the variable “name” so I can do substring operations on it (you can’t do substring operations on iterator variables, so you have to slide them into a regular variable). I then drop the last four characters of the name (!name:~-4,4!, which is a substring specifying an offset into the string of -4, for a substring of 4 characters long) into a temporary file called c:\suffix.txt. I’ve not snagged all of my file suffixes.

In Phase 2, I make a list of unique suffixes, again using the technique I described in detail in Episode 91. I start by sorting my suffix list into a separate temporary file (sort c:\suffix.txt > c:\sortsuf.txt). I then create a variable called previous, which I set to a space just to get started (set previous= ). I then have a FOR /F loop, which iterates over my sorted suffixes using an iterator variable of %j: “for /f %j in (c:\sortsuf.txt)”. In the body of my do loop, I store the current suffix (%j) in a value called current so I can do compares against it. You can’t do compares of iterator variable values, so I’ve gotta tuck %j into the “current” variable. Using an IF statement, I then check to see if my current value is NOT equal to my previous value (if NOT !current!==!previous!). If it isn’t, it means this suffix is unique, so I drop it into a third temporary file, called C:\uniqsuf.txt). I then set my new previous to my current value (set previous=!current!), and iterate. Phase 2 is now done, and I have a list of unique file suffixes.

Finally, in Phase 3, I simply invoke my third FOR /F loop, with an iterator variable of %k, iterating over the contents of my uniqsuf.txt file. For each unique suffix, the do clause of this loop first echo’s the suffix name followed by a colon (echo %k: ). Then, I run something very similar to my original plan for this episode. It’s a dir /b /s command to get a bare form of output (1 line per file), recursing subdirectories, looking for files with the name of *%k. I pipe that output into a little line counter I’ve used in tons of episodes (find /c /v “”), which counts (/c) the number of lines that do not have (/v) nothing (“”). The number of lines that do not have nothing is the number of lines. The output of the find command is displayed on the screen.

After this finishes, I’ve got some clean-up to do. I use the del command to remove the three temporary files I’ve created (c:\suffix.txt, c:\sortsuf.txt, and c:\uniqsuf.txt). And, voila! I’m done.

See, I told you it was straight forward!

For once Hal isn't on the road

While Tim and Ed are whooping it up in Baltimore, I'm relaxing here in the Fortress of Solitude. They're killing brain cells partying it up with all the hot InfoSec pros, while I'm curled up with my Unix command-line to keep me company. No sir, I sure don't envy them one bit.

Since Tim mentions the file command, I suppose I better discuss why I didn't use it for this week's challenge. The problem with file for this case is that the program is almost too smart:

$ file index.html 01_before_pass.JPG Changelog.xls For508.3_4.*
index.html: HTML document text
01_before_pass.JPG: JPEG image data, JFIF standard 1.01
Changelog.xls: CDF V2 Document, Little Endian, Os: Windows, Version 6.0, Code page: 1252,
Author: Kimie Reuarin, Last Saved By: Robin, Name of Creating Application: Microsoft Excel,
Last Printed: Wed Aug 13 21:22:28 2003, Create Time/Date: Mon Aug 11 00:16:07 2003,
Last Saved Time/Date: Wed Jan 6 00:27:30 2010, Security: 0
For508.3_4.pptx: Zip archive data, at least v2.0 to extract
For508.3_4.pdf: PDF document, version 1.6

The output of file gives me a tremendous amount of information about each type of file. However, the output is so irregular that it would be difficult to sort all of the similar file types together.

So I'm going with file extensions, just like the Windows guys. First, you can easily look for a specific extension just by using find:

$ find ~/Documents -type f -name \*.jpg
/home/hal/Documents/My Pictures/IMAGE_00004.jpg
/home/hal/Documents/My Pictures/kathy-web-cropped.jpg
/home/hal/Documents/My Pictures/hal-headshot.jpg
[...]

Here I'm finding all regular files ("-type f") whose name matches "*.jpg". Since the "*" is a special character, it needs to be backwhacked to protect it from being interpolated by the shell (I could have used quotes here instead if I had wanted).

Of course, some of my JPEG files might be named ".jpeg" or even ".JPG" or ".JPEG", so perhaps some egrep is in order:

$ find ~/Documents -type f | egrep -i '\.jpe?g$'
[...]

But the real challenge here is to enumerate all of the file extensions under a given directory. I'm able to extract the extensions using a little sed fu:

$ find ~/Documents -type f | sed 's/.*\.\([^\/]*\)$/\1/'
pdf
pdf
pdf
doc
gif
db
[...]
/home/hal/Documents/Manuals/aaa14612
[...]

The first part of the sed regex, ".*\.", matches everything up to the last dot in the pathname because the "*" operator is "greedy" and will consume as many characters as possible while still allowing the regex to match. Then the remainder, "\([^\/.]*\)$", matches all non-slash characters up to the end of the line. I specifically wrote the expression this way so I wouldn't match things like "/fee/fie/fo.fum/filename". We use the sed substitution operator here ("s/.../\1/") to replace the file name we get as input with the extension that we matched in the "\(...\)" grouping operator.

The only problem is that some of my files don't have extensions or any dot at all in the file name. In this case, the regex doesn't match and the substitution doesn't happen. So you just get the full, unaltered file path as output as you see above. So what I'm going to do is add another sed expression that simply changes any file names containing "/" to just be "other":

$ find ~/Documents -type f | sed 's/.*\.\([^\/]*\)$/\1/; s/.*\/.*/other/'
pdf
pdf
pdf
doc
gif
db
[...]
other
[...]

At this point, getting the summary by file extension is just a matter of a little sort and uniq action:

$ find ~/Documents -type f | sed 's/.*\.\([^\/]*\)$/\1/; s/.*\/.*/other/' | \
sort | uniq -c | sort -nr

1156 jpg
877 ppt
629 doc
315 html
213 other
[...]
56 JPG
[...]
6 html~
[...]
1 html?openidserver=1
[...]

Here I'm using the first sort to group all the extensions togther, then counting them with "uniq -c", and finally doing a reverse numeric sort of the counts ("sort -nr") to get a nice listing.

As you can see, however, there are a few problems in the output. First, I'm counting "jpg" and "JPG" files separately, when they should probably be counted as the same. Also, there are some files extensions with funny trailing characters that should probably be filtered off. The fix for the first problem is to just use tr to fold everything to lowercase before processing. Fixing the second problem can be done by adjusting our first sed expression a bit:

$ find ~/Documents -type f | tr A-Z a-z | \
sed 's/.*\.\([a-z0-9]*\)[^\/]*$/\1/; s/.*\/.*/other/' | \
sort | uniq -c | sort -nr

1212 jpg
878 ppt
631 doc
322 html
213 other
[...]

Now inside of the "\(...\)" grouping in my sed expression I'm explicitly only matching alphanumeric characters (I only have to match lower-case letters here because tr has already shifted all the upper-case characters to lower-case). Everything else after the alphanumeric characters just gets thrown away. Note that when I'm matching "everything else", I'm still being careful to only match non-slash characters.

I realize the sed expressions end up looking pretty gnarly here. But it's really not that difficult if you build them up in pieces. Other than that, the solution is nice and straightforward, and uses idioms that we've seen in plenty of other Episodes.

For those of you who don't like all the sed-ness here, loyal reader Jeff Haemer suggests the following alternate solution:

$ find ~/Documents -type f | while read f; do echo ${f##*/*.}; done | grep -v / | 
sort | uniq -c | sort -nr

1156 jpg
877 ppt
629 doc
321 html
147 pdf
[...]

The trick here is the "${f##*/*.}" construct, which strips the matching shell glob out of the value of the variable "$f". The "##" in the middle of the expression means "match as much as possible", so that basically emulates the greedy "maximal matching" behavior that we were relying on in our sed example.

You'll notice that Jeff's example doesn't do the fancy mapping to "other" for files that don't have an extension. Here he's just using "grep -v" to filter out any pathnames that end up still having a slash in them. We could use a little sed to fix that up:

$ find ~/Documents -type f | while read f; do echo ${f##*/*.}; done | 
sed 's/.*\/.*/other/' | sort | uniq -c | sort -nr

1156 jpg
877 ppt
629 doc
321 html
214 other
[...]

Jeff's code also doesn't deal with the "funny trailing characters" issue, but that's not a huge deal here. Nice work, Jeff!

Tuesday, June 8, 2010

Episode #98: Format This!

Hal is busy

Lately I've found myself having to make lots of file systems. This is mostly due to forensic work, where I'm either sanitizing hard drives and rebuilding file systems on them or I'm creating test file systems for research. Either way, I'm spending lots of time fiddling with file systems at the command line.

Way back in Episode 32 we talked about how to use dd to overwrite a disk device with zeroes:

# dd if=/dev/zero of=/dev/sdd bs=1M
dd: writing `/dev/sdd': No space left on device
992+0 records in
991+0 records out
1039663104 bytes (1.0 GB) copied, 299.834 s, 3.5 MB/s

Of course, this leaves you with an invalid partition table. Happily, the GNU parted utility makes short work of creating a new MS-DOS style disk label and adding a partition:

# parted /dev/sdd print
Error: /dev/sdd: unrecognised disk label
# parted /dev/sdd mklabel msdos
Information: You may need to update /etc/fstab.

# parted /dev/sdd mkpart primary 1 1G
Information: You may need to update /etc/fstab.

# parted /dev/sdd print
Model: LEXAR JUMPDRIVE SPORT (scsi)
Disk /dev/sdd: 1040MB
Sector size (logical/physical): 512B/512B
Partition Table: msdos

Number Start End Size Type File system Flags
1 32.3kB 1036MB 1036MB primary

At this point we need to create a file system in our new partition. You actually can use parted to create file systems, but even the parted manual page suggests that you use an external program instead. In Linux, this would be mkfs, which allows you to choose between several different kinds of file systems.

Since this is a small USB key, you might want to just create a FAT file system on it to make it easy to share files between your Linux box and other, less flexible operating systems:

# mkfs -t vfat -F 32 /dev/sdd1

We're using the FAT-specific "-F" option to specify the FAT cluster address size-- here we're creating a FAT32 file system. For each file system type, mkfs has a number of special options specific to that file system. You'll need to read the appropriate manual page to see them all: "man mkfs.vfat" in this case.

If I didn't want my co-authors to be able to easily see the files on this USB stick, I could create an EXT file system instead:

# mkfs -t ext2 /dev/sdd1
mke2fs 1.41.9 (22-Aug-2009)
Filesystem label=
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
63360 inodes, 253015 blocks
12650 blocks (5.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=260046848
8 block groups
32768 blocks per group, 32768 fragments per group
7920 inodes per group
Superblock backups stored on blocks:
32768, 98304, 163840, 229376

Writing inode tables: done
Writing superblocks and filesystem accounting information: done

This filesystem will be automatically checked every 30 mounts or
180 days, whichever comes first. Use tune2fs -c or -i to override.

Here I'm creating an "ext2" file system because I didn't want to waste space on a file system journal, but you of course have the option of creating "ext3" and even "ext4" file systems if you want.

If you want to make NTFS file systems, you may have to download an additional package. For example, on my Ubuntu laptop I had to "sudo apt-get ntfsprogs". Once that's done, making NTFS volumes is a snap:

# mkfs -t ntfs -Q /dev/sdd1
Cluster size has been automatically set to 4096 bytes.
Creating NTFS volume structures.
mkntfs completed successfully. Have a nice day.

When creating NTFS volumes, you definitely want to use the "-Q" (quick) option. If you leave off the "-Q" then the mkfs.ntfs program overwrites the device with zeroes and performs a bad block check before creating your file system. This takes a really long time, particularly on large drives, and is also unnecessary in this case since we previously overwrote the drive with zeroes using dd.

It's interesting to note that you don't actually have to have a physical disk device to test file systems. mkfs will (grudgingly) create file systems on non-device files:

# dd if=/dev/zero of=testfs bs=1M count=4096
4096+0 records in
4096+0 records out
4294967296 bytes (4.3 GB) copied, 69.6688 s, 61.6 MB/s
# mkfs -t ntfs -Q -F testfs
testfs is not a block device.
mkntfs forced anyway.
[...]
# mount -o loop,show_sys_files testfs /mnt/test
# ls /mnt/test
$AttrDef $Bitmap $Extend $MFTMirr $UpCase
$BadClus $Boot $LogFile $Secure $Volume

Here I'm first using dd to make a file called "testfs" that contains 4GB of zeroes. Then I call mkfs on the file, using the "-F" (force) option so that it won't exit with an error when I tell it to operate on a non-device file. Though the command whines a lot, it does finally produce a working NTFS file system that can be mounted using a loopback mount.

Of course I can create EXT and FAT file systems in a similar fashion. However, the "-F" option for mkfs.vfat is used to specify the cluster address size. It turns out that you don't need a "force" option when making FAT file systems in non-device files-- the mkfs.vfat will create file systems without complaint regardless of the type of file it is pointed at. For EXT file systems, you can use "-F" if you want. However, if you leave the option off, you'll get a "are you sure?" prompt when running the command against a non-device file (as opposed to mkfs.ntfs which simply bombs out with an error). They say that "the wonderful thing about standards is that there are so many to choose from", but I really wish Linux could rationalize the various mkfs command-line interfaces a bit more.

In any event, being able to create file systems in raw disk files is a real boon when you want to test file system behavior without actually having to commandeer a physical disk drive from someplace. But I think I'd better stop there-- I'm already feeling the hatred and jealousy emanating from my Windows brethren. Let's see what Tim can cook up this week.

Tim was relaxing this weekend for his birthday

This week's episode is pretty easy, but only because there aren't a lot of options. Besides, why would you want to create raw disk files or test file system behavior without searching for a physical disk, connectors, power, ...

No, I'm not jealous. I have everything I need. I don't need all those options. Windows is good enough, smart enough, and doggone it, people like it!

The "streamlined" command in Windows is the good ol' Format command.

C:\> format d:

WARNING, ALL DATA ON NON-REMOVABLE DISK
DRIVE D: WILL BE LOST!
Proceed with Format (Y/N)? y
In Vista and later, the format command writes zeros to the entire disk when a full format is performed. In XP and earlier, the format command does not zero the disk. To zero the disk with XP you have to use the diskpart utility.

C:\> diskpart

Microsoft DiskPart version 6.1.7600
Copyright (C) 1999-2008 Microsoft Corporation.
On computer: MYMACHINE

DISKPART> list disk

Disk ### Status Size Free Dyn Gpt
-------- ------------- ------- ------- --- ---
Disk 0 Online 149 GB 0 B
Disk 1 Online 149 GB 0 B

DISKPART> select disk 1

Disk 1 is now the selected disk.

DISKPART> clean all
The clean all command within diskpart zeros the entire disk. One benefit of using clean all is that it actually zeros the disk and doesn't create the MFT. We usually want one though, so Format will suffice.

Format can be used to specify the file system too. We don't have all the options hassles of lots of choices such as EXT. If a file system isn't specified, the Format command uses the volume type to determine the default format for the disk. To explicitly specify the file system use the FS option.

C:\> format e: /FS:NTFS
C:\> format f: /FS:FAT32
Besides the size restriction, one of the biggest problems with the FAT file system is that it provides no security features. If a user has access to the disk then they have full access to the disk, i.e. there is no way to give a user read access and deny write access to a directory. NTFS allows finer control of ACLs, or even ACLs at all.

So how do we convert a FAT drive to NTFS? But of course, by using the convert command:

C:\> convert f: /FS:NTFS
The FS switch is required even though the only option is NTFS.

That's about it. Not a lot here these week, and no PowerShell either. There aren't any new cmdlets in PowerShell that provide any additional functionality.

Tuesday, June 1, 2010

Episode #97: Make me a Sandwich

Tim

One of the best ways to protect your computer is to run with lower permissions (not root or admin). Lots of security problems can be mitigated with this principle. While running as a regular human, we sometimes need to call upon great power to do some tasks. But remember, with great power, comes great responsibility.

So how do we call upon the super human powers in PowerShell? The command looks like this:

PS C:\> Start-Process "$psHome\powershell.exe" -Verb Runas
-ArgumentList '-command "command to execute"'


For example, I want to start Terminal Services:

PS C:\> Start-Process "$psHome\powershell.exe" -Verb Runas
-ArgumentList '-command "Start-Service TermService"'


Or Stop the service:

PS C:\> Start-Process "$psHome\powershell.exe" -Verb Runas
-ArgumentList '-command "Stop-Service TermService -force"'


The "-Verb Runas" means that the command should be run as the administrator. The Argument List parameter takes the commands to be passed to the elevated instance of PowerShell.

There are a few problems with this. First, depending on the settings in Vista or Windows 7, you will get the UAC popup. Second, this actually creates a new instance of PowerShell with new environment variables and a different working directory. Third, errors are pretty much impossible to read since the session is destroyed upon completion of the command. Forth, the command is pretty long and goofy. Fifth, it isn't so easy to say:

Start-Process "$psHome\powershell.exe" -Verb Runas
-ArgumentList '-command "Make me a Sandwich"'


Here is a simplified function that can be used to call elevated commands. I'd suggest adding it to your profile so it is ready when you need it.

function sudo
{
param( [string]$arguments = $args )
$psi = new-object System.Diagnostics.ProcessStartInfo "$psHome\powershell.exe"
$psi.Arguments = $arguments
$psi.Verb = "runas"
[System.Diagnostics.Process]::Start($psi)
}


Commands can be called like this:

PS C:\> sudo Stop-Service TermService -force


It does have the same limitations as above, execept it is easier to type.

Oh well, in true Windows form, we can run the entire shell as admin via the GUI (meh) or by using Ed's method.

Ed
So, Tim started his section by just saying "Tim". I guess we're into minimalism this week, so I'll just start with "Ed".

You know, this whole topic of elevated command line access comes up in my SANS classes a lot. It doesn't plague students who show up with Windows XP. But, those people who arrive in class with Windows Vista or Windows 7 are sometimes surprised by it. They logon to their laptop GUI as a user in the administrator's group, and then invoke a cmd.exe. Then, they try to run certain commands that alter the operating system configuration, and they get an "Access is denied" message. I'm not talking about UAC here, that delightful little dialog box Windows displays any time you want to do something interesting. I'm talking about not having the privileges to do what you want. Consider this nice little message from my Win 7 box, using the service controller command to try to stop the Windows Search service associated with indexing:
C:\> sc stop wsearch
[SC] OpenService FAILED 5:

Access is denied.
After this occurs in my class, a hand usually goes up, and I get asked a question about why it doesn't work. "You don't have elevated privileges," I respond. "But, I logged in as admin," they often shoot back. "Ahhh, but Microsoft is trying to protect you from yourself. They seem to think that the big, bad, scary command line is just too powerful for someone to use with admin privs unless they explicitly ask for such privs. So, when you logon with an admin account, and launch a cmd.exe, you don't have full admin privs in the resulting command prompt. You need to launch an elevated command prompt."

I then show them how to launch one at the GUI. Simply go to your Windows icon on your tool tray (still called the "Start" menu, but since it doesn't say "Start" anymore, I don't personally call it that). Click on it and do a search for cmd.exe. When you see the icon for cmd.exe pop up, right click on it and select "Run as administrator". Alternatively, you can point your mouse to hover over the cmd.exe and hit CTRL-SHIFT-ENTER to launch it with elevated privileges. I prefer the right-click action myself, because it just feels kinda weird to hover my mouse over something and then hit CTRL-SHIFT-ENTER. When your cmd.exe launches, its title bar will say "Administrator: cmd.exe", giving you a reminder that you have an elevated prompt.

If you find yourself frequently needing an elevated command prompt, you can create a shortcut to cmd.exe and place it on your desktop. Right click on your shortcut, go to Properties, click the "Shortcut" tab, and click "Advanced". Check the "Run as administrator" box. You may want to name your shortcut ElevatedCmd.exe or something to remind you about its use.

Well, this is all well and good, but how do you do launch an elevated command shell at, you know, the command line? Well, for that, we rely on the good old runas command. At a non-elevated prompt, you could simply run:

C:\> runas /u:administrator <command>

When prompted, type in the administrator's password, and you are good to go.

That command can be whatever you'd like, such as the "sc stop wsearch" or "sc start wsearch". Or, you can even launch another, elevated cmd.exe with:

C:\> runas /u:administrator cmd.exe

You know, all this musing about runas and (as Hal is certain to point out) sudo reminds me of a fun conversation I had with fellow InGuardians dude Tom Liston a few years ago. I told him that I was creating a new Windows command called "don't runas". It would take whatever command you specify, and not run it. But, in not running it, it wouldn't just do nothing... it would literally do nothing. It would actually run a bunch of nops, with the privileges of the user you specify. Tom then said we could do a Linux equivalent, called "sudont". Then, in a fit of creativity, we thought about offering a cloud-based Application-as-a-Service version of this, which would allow a user to kick off a dontrunas or sudont on their machine, and it would be submitted via an empty SOAP request to a bunch of servers on the Internet that would do nothing very quickly and in parallel, sending a response back with nothing in it. We decided that we could actually charge for such a service, making big money from users for doing nothing for them. But, then, we realized that we might get sued by various Certificate Authorities for infringing on their business models, so we never really implemented our idea. Which, come to think about it, actually makes sense. We were going to make a dontrunas and sudont command, but we just never got around to doing anything with it.

Hal (just keeping with the theme here, people)

Hey Ed, doesn't the US Congress have the patent on the "doing nothing in parallel" idea? You'd better watch out there. Oh wait, you said "doing nothing quickly and in parallel". I guess you're OK after all.

I'd like to thank my co-authors for serving up another easy one for me this week, as I'm currently in transit between one conference in the next. Of course the command to run a single command with superuser privileges is the venerable sudo command:

$ sudo grep ^hal: /etc/shadow
[sudo] password for hal: <not echoed>
hal:LIKEIMREALLYGOINGTOSHOWYOUMYPASSWORD.YOUMUSTBECRAZY.:14579:0:99999:7:::

sudo prompts the user for their own password. Assuming the system administrator has granted the user sudo access to the command the user is trying to execute, the command will run with elevated privileges.

Of course, those "elevated privileges" need not be root. With the "-u" option, you can specify another user to run your command as:

$ sudo -u mysql ls /var/lib/mysql/mysql
columns_priv.frm help_relation.MYI time_zone_leap_second.frm
[...]
help_keyword.MYI tables_priv.MYD user.frm
help_relation.frm tables_priv.MYI user.MYD
help_relation.MYD time_zone.frm user.MYI

Why wasn't I prompted for my password this time? sudo "remembers" that you typed your password recently and doesn't prompt you again as long as you keep using sudo within a relatively small interval. The default is 5 minutes, but you can customize this in the /etc/sudoers configuration file.

Anyway, I normally try to force my DBAs to use sudo instead of su-ing directly to the user mysql, oracle, etc. Of course they get tired of having to type "-u mysql" on every command. Just FYI, you can put the following in your /etc/sudoers file so that all members of the Unix group "dba" will sudo to the "mysql" user by default:

Defaults:%dba       runas_default = mysql

Of course, those users will now have to explicitly "sudo -u root ..." to do anything as root.

By the way, as Ed mentioned in his section, environment variables getting reset can be a problem when you're using a tool like sudo or runas to do things as an alternate user. Your DBAs are going to have particular problems with this, since most of their scripts are going to assume that they're logged in directly as the "oracle" user or whatever and have all of the environment variable settings that go along with logging into that account. You may want to look into the env_keep option in your /etc/sudoers file to selectively preserve certain environment variable settings your DBAs are expecting to have.

Of course, your DBAs are immediately going to try to "sudo -u oracle /bin/bash" or "sudo -u oracle su" in order to get an interactive shell. At this point they've "escaped from sudo" and you're no longer getting an audit trail of what they're doing. You can try to prevent them from doing this by writing your /etc/sudoers config in such a way as to not allow them to execute these commands, but remember that many Unix commands allow "shell escapes" to an interactive shell:

$ sudo vi
(inside of vi) :shell
# id
uid=0(root) gid=0(root) groups=0(root)...

There is an /etc/sudoers option called "noexec" that you can turn on to disable shell escapes from programs (which it does by some really clever substitution in LD_LIBRARY_PATH). "noexec" is useful although it can break programs like "crontab -e" that rely on being able to exec() your editor to let you edit your crontab.

There's also the "sudoedit" option for allowing people to securely edit privileged files. sudoedit uses superuser privileges to make a copy of the file that is writable by the user, edits the file as the user, and then uses superuser privileges to put the edited file back into place.

One last item bears mentioning as long as we're talking about sudo. Output redirection can be a problem with sudo:

$ cd /etc
$ sudo awk -F: '($2 == "") { print }' /etc/shadow >empty_passwds
bash: empty_passwds: Permission denied

The problem here is that while the awk command happens with superuser privileges, the output redirection to the file empty_passwds happens in a subshell that is not running via sudo. Since your normal user account doesn't have write permissions under /etc, you get the "Permission denied" message.

The work-around is to use "sudo tee" in a pipeline:

$ sudo awk -F: '($2 == "") { print }' /etc/shadow | sudo tee empty_passwds >/dev/null

The tee command writes its input to a file and also to the standard output. In this case, we just care about creating the empty_passwds file, so I redirect the standard output to /dev/null to discard it.

Whew! For an "easy" Episode, I sure ended up packing a lot in here. I hope this helps you with your sudo-ing in the future.

Tuesday, May 25, 2010

Episode #96: Hardware Death Watch

Hal's Laptop Is Having Issues

This is pretty much a SANS instructor's worst nightmare. I'm headed out to teach Forensics 508 in VA Beach, and I fire up my laptop to get some work done on the plane. The CPU fan makes a choking sound, the laptop beeps, and the screen flashes "Fan error". Thankfully, a little gentle coercion rendered the system bootable, but I'm clearly looking at a complete fan failure in the near future. So I want to keep an eye on my hardware so I can prevent an incident that involves the magic smoke.

There are a number of different ways of getting information about your hardware on Linux. The simplest is probably lshw:

# lshw
elk
description: Notebook
product: 7668CTO
vendor: LENOVO
version: ThinkPad X61s
serial: LVA9486
width: 64 bits
capabilities: smbios-2.4 dmi-2.4 vsyscall64 vsyscall32
configuration: administrator_password=disabled boot=normal chassis=notebook...

lshw provides a ton of other info on your BIOS, CPU(s), memory, disk drives, display and so on-- almost 400 lines of output on my laptop! Note that there's also the report-hw command which reports similar information, but was designed to help with debugging hardware auto-detection and so has lots of extra output that makes things less readable overall.

While lshw is good for getting an overview of the hardware configuration of your system, it doesn't probe any of the internal hardware sensors in your computer. To talk to the sensors in your CPU(s) and disk drives, you'll need a couple of other packages that are standard with most Linux distros these days: lm-sensors and smartmontools. lm-sensors interacts with the CPU sensors and smartmontools lets you get information from your disk drives, assuming they're modern enough to support the SMART device interface.

To get started with the lm-sensors package, you'll need to load the appropriate kernel modules for your device. Happily, the package includes a tool called sensors-detect that will auto-detect the kernel modules you need, and even offer to update your configuration so that the appropriate modules will be automatically loaded whenever your system boots. Here's an excerpt from the output of this program:

# sensors-detect
# sensors-detect revision 5249 (2008-05-11 22:56:25 +0200)

This program will help you determine which kernel modules you need
to load to use lm_sensors most effectively. It is generally safe
and recommended to accept the default answers to all questions,
unless you know what you're doing.

We can start with probing for (PCI) I2C or SMBus adapters.
Do you want to probe now? (YES/no): yes
[...]

Now follows a summary of the probes I have just done.
Just press ENTER to continue:

Driver `coretemp' (should be inserted):
Detects correctly:
* Chip `Intel Core family thermal sensor' (confidence: 9)

I will now generate the commands needed to load the required modules.
Just press ENTER to continue:

To load everything that is needed, add this to /etc/modules:

#----cut here----
# Chip drivers
coretemp
#----cut here----

Do you want to add these lines automatically? (yes/NO) yes
# cat /etc/modules
# /etc/modules: kernel modules to load at boot time.
#
# This file contains the names of kernel modules that should be loaded
# at boot time, one per line. Lines beginning with "#" are ignored.

loop
lp
rtc

# Generated by sensors-detect on Sun May 23 10:59:47 2010
# Chip drivers
coretemp

Once the appropriate drivers are loaded, you can just run the sensors command-- and you don't even have to be root:

$ sensors
acpitz-virtual-0
Adapter: Virtual device
temp1: +39.0°C (crit = +127.0°C)
temp2: +39.0°C (crit = +100.0°C)

thinkpad-isa-0000
Adapter: ISA adapter
fan1: 3872 RPM
fan2: 0 RPM
temp1: +39.0°C
temp2: +46.0°C
temp3: +46.0°C
temp4: +37.0°C
ERROR: Can't get value of subfeature temp5_input: Can't read
temp5: +0.0°C
ERROR: Can't get value of subfeature temp6_input: Can't read
temp6: +0.0°C
ERROR: Can't get value of subfeature temp7_input: Can't read
temp7: +0.0°C
ERROR: Can't get value of subfeature temp8_input: Can't read
temp8: +0.0°C
temp9: +42.0°C
temp10: +38.0°C
ERROR: Can't get value of subfeature temp11_input: Can't read
temp11: +0.0°C
ERROR: Can't get value of subfeature temp12_input: Can't read
temp12: +0.0°C
ERROR: Can't get value of subfeature temp13_input: Can't read
temp13: +0.0°C
ERROR: Can't get value of subfeature temp14_input: Can't read
temp14: +0.0°C
ERROR: Can't get value of subfeature temp15_input: Can't read
temp15: +0.0°C
ERROR: Can't get value of subfeature temp16_input: Can't read
temp16: +0.0°C

coretemp-isa-0000
Adapter: ISA adapter
Core 0: +39.0°C (high = +100.0°C, crit = +100.0°C)

coretemp-isa-0001
Adapter: ISA adapter
Core 1: +39.0°C (high = +100.0°C, crit = +100.0°C)

Clearly, not all temperature sensors are supported on all CPU architectures. But at least this allows me to keep up my morbid death watch on my fans and my CPU temp.

The smartmontools package includes the smartctl command for probing your disk drives. The easiest way to get started is to just use the "-a" option to dump all available info about your drive:

# smartctl -a /dev/sda
smartctl version 5.38 [x86_64-unknown-linux-gnu] Copyright (C) 2002-8 Bruce Allen
Home page is http://smartmontools.sourceforge.net/

=== START OF INFORMATION SECTION ===
Device Model: ST9500420AS
Serial Number: 5VJ09ARF
Firmware Version: 0002SDM1
User Capacity: 500,107,862,016 bytes
Device is: Not in smartctl database [for details use: -P showall]
ATA Version is: 8
ATA Standard is: ATA-8-ACS revision 4
Local Time is: Sun May 23 11:11:33 2010 PDT
SMART support is: Available - device has SMART capability.
SMART support is: Enabled

=== START OF READ SMART DATA SECTION ===
SMART overall-health self-assessment test result: PASSED

[...]

SMART Attributes Data Structure revision number: 10
Vendor Specific SMART Attributes with Thresholds:
ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE
1 Raw_Read_Error_Rate 0x000f 118 099 006 Pre-fail Always - 188548774
3 Spin_Up_Time 0x0003 100 098 085 Pre-fail Always - 0
4 Start_Stop_Count 0x0032 100 100 020 Old_age Always - 269
5 Reallocated_Sector_Ct 0x0033 100 100 036 Pre-fail Always - 0
7 Seek_Error_Rate 0x000f 074 060 030 Pre-fail Always - 27946375
9 Power_On_Hours 0x0032 098 098 000 Old_age Always - 2501
10 Spin_Retry_Count 0x0013 100 100 097 Pre-fail Always - 0
12 Power_Cycle_Count 0x0032 100 037 020 Old_age Always - 202
184 Unknown_Attribute 0x0032 100 100 099 Old_age Always - 0
187 Reported_Uncorrect 0x0032 100 100 000 Old_age Always - 0
188 Unknown_Attribute 0x0032 100 099 000 Old_age Always - 121
189 High_Fly_Writes 0x003a 100 100 000 Old_age Always - 0
190 Airflow_Temperature_Cel 0x0022 061 051 045 Old_age Always - 39 (Lifetime Min/Max 28/39)
191 G-Sense_Error_Rate 0x0032 100 100 000 Old_age Always - 0
192 Power-Off_Retract_Count 0x0032 100 100 000 Old_age Always - 8
193 Load_Cycle_Count 0x0032 098 098 000 Old_age Always - 5470
194 Temperature_Celsius 0x0022 039 049 000 Old_age Always - 39 (0 11 0 0)
195 Hardware_ECC_Recovered 0x001a 047 043 000 Old_age Always - 188548774
197 Current_Pending_Sector 0x0012 100 100 000 Old_age Always - 0
198 Offline_Uncorrectable 0x0010 100 100 000 Old_age Offline - 0
199 UDMA_CRC_Error_Count 0x003e 200 200 000 Old_age Always - 0
240 Head_Flying_Hours 0x0000 100 253 000 Old_age Offline - 115611929676228
241 Unknown_Attribute 0x0000 100 253 000 Old_age Offline - 978381419
242 Unknown_Attribute 0x0000 100 253 000 Old_age Offline - 1156631671
254 Unknown_Attribute 0x0032 100 100 000 Old_age Always - 0
[...]

Again, there's a ton of other output from this command, which I'm not showing in the interests of space. There are smartctl options to just dump out specific pieces of the above info, and they're all documented in the manual page.

Frankly, I think it's pretty cool that I can retrieve the disk model and serial number without having to crack the case. I can also read the temp of the drive itself and at the airflow output, which is of interest to me right now. But as you can see, I can also get information about the number of hours on the drive and so on. This could be used to help alert you to drives that may be needing replacement before they actually fail.

So there's plenty of information available for me to keep an eye on things this week as I'm teaching my class. Keep your fingers crossed for me. In the meantime, let's see what Ed and Tim have up their sleeves.

Ed responds a little nervously:
I get shivers thinking about system failure as a presenter at conferences. I used to travel with two laptops to keep my mind at ease, but in the past year, I started carrying just one as my back started to hurt. Rumor has it that there are backup laptops that will materialize in an instant at a conference, but you never know. USB tokens with backup presentations are a good idea.

As for commands to check hardware information on Windows, our little friend WMIC comes in handy. For details about the motherboard, we could run:
C:\> wmic baseboard list full

The output here will show us Manufacturer and SerialNumber, among other things.

For CPU information, we can run:
C:\> wmic cpu list full

This will show us a description of the CPU, its manufacturer, and speed.

But, in that glut of output, there are also a couple of useful items that may indicate trouble on our system. Let's zoom in on them:
C:\> wmic cpu get currentclockspeed,maxclockspeed

If you see a big difference in these numbers, it could be due to a couple of reasons. First off, your system may be running under a low power condition, so it slows down the processor to save power, making currentclockspeed lower than maxclockspeed. That's nothing to worry about. The other condition, however, is that your system has gotten kinda hot, so it's slowing itself down. That's something to worry about.

To get a feel for the temperature of your system, you could run:
C:\> wmic /namespace:\\root\wmi PATH MSAcpi_ThermalZoneTemperature get 
CurrentTemperature
CurrentTemperature
3172
Now, it should be noted that pulling this temperature data isn't supported on all hardware, and on some hardware, it never changes beyond boot time. Still, on many modern non-virtual systems, it'll tell you your temperature in tenths of degree Kelvin. I just went to Google and did a search for "317.2 degrees kelvin to" and before I finished typing, the predictive search responded with:
317.2 kelvin = 44.05 degrees Celsius

Cool, Google. A little creepy, but cool. "Google: A little creepy, but cool" should be Google's new motto, supplanting "Don't be Evil."

Of course, then, I type "44.05 degrees Celsius to f" and it pops up and tells my system is running at 111.29 degrees Fahrenheit. Toasty.

The ScriptInternals guys have put together a list of the items you can read using this command besides the CurrentTemperature. You can pull all of this data with:

C:\> wmic /namespace:\\root\wmi PATH MSAcpi_ThermalZoneTemperature get *

While all this temperature stuff is nice, what about a prediction of whether our hard drive is hosed? We can pull that information with:

C:\> wmic /namespace:\\root\wmi PATH MSStorageDriver_FailurePredictStatus get 
predictfailure
PredictFailure
FALSE

Whew, that's a relief. If this output says TRUE, your drive is ready to give up the ghost soon, so you should backup immediately! You don't want to fall into the "Hal Pomeranz conference laptop deathwatch trap".

Tim sometimes wish a presenter's laptop would die:

We've all been there, a presentation where the presenter is just reading every word on every slide with no extra content or commentary. That presenter's laptop need to die, to take one for the team so the rest of us can live.

I've heard Ed and Hal present, both are great speakers, so their laptops are not required to become martyrs. Let's give them a bit of a check up.

Checking a laptop's status in PowerShell is very similar to what Ed did. Here are the PowerShell versions of Ed's commands.

Motherboard - Manufacturer and Serial Number:
PS C:\> gwmi win32_baseboard


CPU information - Description, Manufacturer, and Speed:
PS C:\> gwmi win32_processor


Temperature:
PS C:\> Get-WmiObject -class MSAcpi_ThermalZoneTemperature -Namespace root\WMI


We have the same problem as Ed, Kelvin. Let's convert to Fahrenheit. My undergraduate degree was in Engineering, and I had to take a Thermodynamics class. One thing I remember is that 0 Kelvin is 273.15 Celsius. I also remember how to convert Celsius to Fahrenheit: add 40, multiply by 9, divide by 5, and finally subtract 40. Here it is in only line.

PS C:\> (((Get-WmiObject -class "MSAcpi_ThermalZoneTemperature" -Namespace
"root\WMI").CurrentTemperature / 10 - 233.15) * 9 / 5) - 40

124.79


Let's check the drive status:
PS C:\> Get-WmiObject -class MSStorageDriver_FailurePredictStatus -Namespace root\WMI | Select Active, PredictFailure

Active PredictFailure
------ --------------
True False


Good news, the drive is alive, and not predicted to die!

One other think I like to check is the battery:

PS C:\> gwmi Win32_Battery | select est*

EstimatedChargeRemaining EstimatedRunTime
------------------------ ----------------
97 231


I can run for almost 4 hours. That's a long presentation, and a lot of slides to read.

Tuesday, May 18, 2010

Episode #95: I Screen, You Screen, We All Screen for...

Ed's Tan, Rested, and Ready:

I'm back from vacation, and wanted to thank my fellow CLKF'ers for holding down the fort while I was away. Tim and Hal did a bang up job responding to the hundreds of thousands of e-mails from adoring fans, managing the hordes of Bodacious Research Assistants on the 83rd floor of Kung Fu Towers (our skyscraper that holds the world-wide headquarters of our blog and the infrastructure necessary to support it), and dealing with any IT issues that came up in our shop while I was absent. Tim mentioned to me that one of these issues dealt with a user whose GUI was giving him problems. He was complaining that the program didn't fit on his screen. Hmmmm... probably an issue with the screen resolution.

We can check the screen resolution with cmd.exe of a remote system using every Window user's best friend at the commandline, wmic, thusly:
C:\> wmic /node:IPaddr /user:Admin /password:Password desktopmonitor
get screenwidth, screenheight


ScreenHeight ScreenWidth
600 800
So, we can see that this GUI was a little tiny by modern standards. Tim provided some verbal coaching to the user about how to change this, and... voila! Problem solved.

Even one's best friends can be annoying sometimes, and wmic certainly has its frustrating parts. Note how we asked for screenwidth followed by screenheight, but wmic gave them to us backwards? That's because wmic always returns attributes in alphabetical order by attribute name (screenheight is alphabetically before screenwidth). The alphabetical fetish is hard coded into wmic, and there's no way around it using wmic by itself. That's why I usually manual alphabetize the attributes I ask for in my wmic commands. It makes me feel like my computer is doing what I want. I ask for them alphabetically, and it gives them to me alphabetically. You see, one doesn't use cmd.exe... it uses you.

But, who wants to look at screen resolutions listed backwards (600X800)? Clearly, alphabetical order here is lame. We've gotta reverse that, which we can do with our other little buddy, the cmd.exe FOR /F loop, a quirky little parser dude:

C:\> for /f "skip=1 tokens=1,2" %i in ('"wmic /node:IPaddr /user:Admin 
/password:password desktopmonitor get screenheight, screenwidth"')
do @echo %jX%i
1024X768
Here, I'm running a FOR /F loop to parse the output of my wmic command. I set my parsing options to skip down 1 line (because I want to bypass the column titles), and tokenize around the first and second columns of my output. My iterator variable will be %i, and because I have two tokens, %j will be automagically allocated. I then include my wmic command, which is inside of single quote double quotes (' "). The single quote tells the FOR loop I'll be executing a command. The double quotes lets me use a command that has special characters in it, such as a comma, without having to resort to the funky ^ character to escape it. It reads a little nicer this way. Just a little.

Note that in my command, I have alphabetized my requested attributes, my standard practice with wmic, so that I can more easily keep in my head their order when dealing with them in the body of my parsing loop. Finally, in the body of the loop (after the "do"), I turn off command display (@) and echo out my variables, reversed, with an X in between (for resolution). So, we see %jX%i, or 1024X768 in this example.

Whew! That's ugly... but it is easily extensible for all kinds of wmic madness.

Furthermore, remember that we can replace our /node:IPaddr with /node:@filename, having a file with one IP address or machine name per line, and we can pull information from a bunch of boxes about their screen resolution.

Unfortunately, there is no way to alter the screen resolution at the cmd.exe command line using only built-in tools. The wmic desktopmonitor alias has no callable methods, nor does the desktop alias. There are some great third party tools for doing so, like Display Changer, which is free for personal and educational use.

Tim is pale, tired, and slow

Those silly people and their GUI's. All sorts of problems with color and resolution. Ironically, the problem was found via the command line since the user wasn't able to determine the resolution he was running.

Here is the PowerShell version of the command. It is very similar to Ed's command, except it will (usually) prompt for credentials via a dialog box (GUI). The credentials are stored in a secure string. A secure string is encrypted in memory and zero'ed when no longer used.

PS C:\> $cred = Get-Credential
PS C:\> Get-WmiObject win32_desktopmonitor -ComputerName GuiMachine -Credential $cred |
select screenwidth, screenheight

screenwidth screenheight
----------- ------------
800 600
One noticeable difference between wmic and Get-WmiObject (alias gwmi) is that the full class name has to be used in PowerShell. This means that you typically have to type Win32_ (case insensitive) before the class name.

We can shorten this command to one line as well as use aliases and shortened parameter names.

PS C:\> gwmi win32_desktopmonitor -comp GuiMachine -cred (Get-Credential) | select screen*
screenheight screenwidth
------------ -----------
600 800
Let's take a step back and look at the properties of the $cred variable that holds our credentials.

PS C:\> $cred
UserName Password
-------- --------
sillyuser System.Security.SecureString
Hrm, can we see what the password contains?

PS C:\> ConvertFrom-SecureString $cred.Password
01000000d08c9ddf0115d1118c7a00c04fc297be01000000477d77c
aaec31c478b9568787c422fb10000000002000000000003660000c0
00000010000000232a3a9ecb092c10661956b28dee0f63000000000
4800000a0000000100000009d240c479361e0156ba4b63f995270de
18000000521e807650133832cfe5fc675cf3c7b8f71d4a5b0d4fa1f
114000000da5bfe8edf24c21b17a326989a82dd83ad1fb69c
Nope. There is a way, but it can only be decrypted by the same user on the same machine. This is a much safer option than typing the clear text password on the command line. If you want, you can read the details on DPAPI, but we will go into this more in a future episode.

We can even save the credentials in a file and import them for later use. First, export:

PS C:\> ConvertFrom-SecureString $cred.Passord | Out-File encryptedpass.txt
Then import the password, and recreate the credential.

PS C:\> $pass = ConvertTo-SecureString (cat encryptedpass.txt)
PS C:\> $cred = New-Object System.Management.Automation.PSCredential
-ArgumentList "myuser",$pass
You can even use another key to encrypt the exported file by using the -key parameter.

The only goofy thing with Get-Credential is that it pops up a dialog box to prompt for the credentials, silly GUI's. You can edit the registry to change the behavior so it prompts on the command line.

PS C:\> Set-ItemProperty HKLM:\SOFTWARE\Microsoft\PowerShell\1\ShellIds
-Name ConsolePrompting -Value True
Now we see the prompt on the command line.

PS C:\> PS C:\> $cred = Get-Credential
Supply values for the following parameters:
Credential
User: myuser
Password for user myuser: ****************
If we wanted to get the resolution on a number of machines we can use the following command.

PS C:\> Get-Content servers.txt |
% { gwmi win32_desktopmonitor -comp $_ -cred $cred } |
select SystemName, ScreenWidth, ScreenHeight

SystemName ScreenWidth ScreenHeight
---------- ----------- ------------
Machine1 800 600
Machine2 640 480
Machine3 1440 900
Machine4 1024 768
Let's see how easy this is for Hal...
Hal Isn't Sure Which End Is Up:

It turns out there are a couple of answers to the "What's my screen resolution?" question on a typical Unix system running some X Windows based display. First there's the old, reliable xdpyinfo command. To tell you just how old this command is, I can remember that one of the first shell scripts I ever wrote back in the 1980's parsed the output of xdpyinfo when setting up my default windowing environment. xdpyinfo dumps out a ton of information-- some useful and some not so much-- but here's a quick idiom for grabbing the screen resolution from the output:

$ xdpyinfo | awk '/dimensions:/ {print $2}'
1920x1200

And, yes, that's "width x height" unlike the Windows "standard" ordering. Crazy Unix people, what will they think of next?

However, the modern mechanism for interacting with your display(s) is the xrandr command. Short for "X Rotate and Resize", xrandr lets you query the current state of the display but, as you might guess from the command name, is really designed to allow you to manipulate the display from the command line or from within a shell script.

You can output the current display info with "xrandr -q":

$ xrandr -q
Screen 0: minimum 320 x 200, current 1920 x 1200, maximum 8192 x 8192
VGA1 connected 1920x1200+0+0 (normal left inverted right x axis y axis) 519mm x 324mm
1920x1200 60.0*+
1280x1024 75.0
1024x768 75.1 60.0
800x600 75.0 60.3
640x480 75.0 60.0
720x400 70.1
LVDS1 connected (normal left inverted right x axis y axis)
1024x768 50.0 + 85.0 75.0 70.1 60.0 40.0
832x624 74.6
800x600 85.1 72.2 75.0 60.3 56.2
640x480 85.0 72.8 75.0 60.0 59.9
720x400 85.0
640x400 85.1
640x350 85.1
0x0 0.0

This is the output from my laptop in its configuration in my office, where I have it connected to an external display ("VGA1" in the xrandr output) in addition to its internal video display ("LVDS1" for Laptop Video Display System). You can see all of the supported resolutions for each display. The "*" marks the active display(s)-- here I'm only using my external monitor at 1920x1200 just like we saw in the xdpyinfo output.

But the power of xrandr is its ability to completely control how your displays are set up. For example, here's the xrandr command I use when I'm teaching and I want my laptop display and the external projector to be showing the exact same image:

xrandr --output LVDS1 --mode 1024x768 --output VGA1 --mode 1024x768 --same-as LVDS1

But the two displays don't have to be showing the same image:

xrandr --output VGA1 --auto --output LVDS1 --auto --right-of VGA1

Here the "--auto" after each display means "choose the highest available resolution": 1920x1200 in the case of my external monitor and 1024x768 for my laptop display. And note that instead of "--same-as" I'm using "--right-of" to position the laptop display virtually to the right of my external monitor (where it sits physically on my desk). The upshot is that I can drag windows off the right-hand side of my external monitor and they'll show up on my laptop display. It's kind of cool, but my laptop display is really too small to be of much use when I'm working at my desk. By the way, there's also "--left-of", "--above", and "--below" positioning options, just like you might expect.

If I want to reset things to my default desktop environment-- laptop display off and external monitor at max resolution-- all I need to do is:

xrandr --output LVDS1 --off --output VGA1 --auto

But suppose this was a desktop machine with dual displays. Personally, I prefer to run my dual displays in "portrait" mode (more code in my display windows that way):

xrandr --output VGA1 --auto --rotate right \
--output VGA2 --auto --rotate right --right-of VGA1

The "--rotate" option handles orienting the display into portrait mode, and you can go either "right" or "left", depending how your monitor mount swivels. There's even "--rotate inverted" which I suppose might be useful if you're trying to display from a projector suspended upside-down from the ceiling (though most projectors these days have an internal setting to deal with that).

I have to say that xrandr is one of the coolest things to happen in X Windows for a while. It used to be much more painful to manipulate display configurations. But now it's totally straightforward.

Tuesday, May 11, 2010

Episode #94: A Date With Death

Hal checks into the mailbag

We received a note recently from a new reader, Ray Kano, who had a question for the blog:
Is there any way using WMIC to write a taskkill command that will kill [processes by name and] based on a date-time stamp?

Now obviously Ray is looking for a Windows solution, and I'll let Tim clean up on that side of the house since Ed is still on vacation. But the question got me thinking if there was an analogous command on Unix for killing processes by name and by date. This turns out to be a lot harder in Unix than I thought it would be, but I learned a lot in the process of figuring out the solution.

My first thought was to do something clever with /proc. I had just assumed that the date-time stamps on the /proc/<pid> directories corresponded with the date the process was spawned. Nothing could be further from the truth:

# uptime
14:55:26 up 23:51, 6 users, load average: 0.43, 0.26, 0.19
# date
Sun May 2 14:55:28 PDT 2010
# stat /proc/1
File: `/proc/1'
Size: 0 Blocks: 0 IO Block: 1024 directory
Device: 3h/3d Inode: 533233 Links: 7
Access: (0555/dr-xr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2010-05-02 14:55:31.256904804 -0700
Modify: 2010-05-02 14:55:31.256904804 -0700
Change: 2010-05-02 14:55:31.256904804 -0700

Note that the system has been up just under a day, but the MAC times on the /proc/1 directory belonging to the init process are all set to the moment I ran the stat command to retrieve the data. Now I did this test on my Linux system and haven't checked other Unix platforms, but clearly relying on /proc isn't going to be a portable solution.

My next thought was to check and see if the killall or pkill commands had options for selecting processes based on date and time. It turns out pkill has the "-o" and "-n" options for killing the oldest or newest processes that match your search criteria, but nothing more selective than that. killall is no help at all.

If you've been reading this blog for a while, you can probably guess where I went next: my "little friend" lsof. But guess what? As far as I can tell, lsof has no capability to even output the starting date and time of a process, much less select processes based on that information.

This was starting to really interest me now. I was sure that the kernel keeps track of the starting date and time of each process, but there didn't seem to be any simple way of getting at this data. In desperation, I started reading the ps manual page and discovered that you can get ps to output a couple of different time values: the "start_time" and the "etime", which is short for "elapsed time". Let's check out "start_time" first with the ps output from one my Linux servers that's been up for a while:

# $ ps -eo pid,comm,start_time
PID COMMAND START
1 init 2009
...
1009 sendmail Jan04
1020 sendmail Jan04
...
29399 sshd 12:11
29401 sshd 12:11
...

The "-o" option allows me to specify a list of fields to output. Note that the names of the various fields and the list of available fields can vary from OS to OS, but the ones I'm using here are pretty standard across many Unix variants.

But.. *yuck*! The output format here is not helpful at all. Processes that were started today show up with HH:MM format. But processes started yesterday or earlier just show up as MonDD, and processes started before Jan 1 of the current year show up as YYYY. I can't do anything useful with this stuff.

Keeping my fingers crossed, I tried using "etime" instead of "stime":

# ps -eo pid,comm,etime
PID COMMAND ELAPSED
1 init 369-07:14:45
...
1009 sendmail 118-01:28:11
1020 sendmail 118-01:28:10
...
29399 sshd 03:47:33
29401 sshd 03:47:31
...
29777 ps 00:00
...

OK, I can work with this. The elapsed time format is [[days-]HH:]MM:SS, which is still kind of a pain but not impossible. I can easily break each line up into a number of tokens. But the problem is that sometimes minutes and seconds will be the third and fourth tokens, sometimes the fourth and fifth tokens, and sometimes even the fifth and sixth tokens. Life would be better if we could reverse the time format so that it was SS:MM[:HH[-days]], which would make everything nice and regular.

I can handle the necessary field reversal with a little awk fu:

# ps -eo pid,comm,etime | tail -n +2 | sed 's/[-:]/ /g' | \
awk '{print $1, $2, $6, $5, $4, $3}'

1 init 59 23 07 369
...
1009 sendmail 25 37 01 118
1020 sendmail 24 37 01 118
...
29399 sshd 47 56 03
29401 sshd 45 56 03
...
29803 ps 00 00
...

Here I'm using the tail command to drop the initial header line and then using sed to turn the dash and colons in the time format to spaces. From there it's a matter of using awk to selectively reverse the last four fields of output. awk doesn't complain if some of the fields don't exist, it simply outputs an empty string.

With the fields now in a canonical order, all I need to do is convert the time value into a format that's useful for comparisons-- like say total elapsed seconds:

# ps -eo pid,comm,etime | tail -n +2 | sed 's/[-:]/ /g' | \
awk '{print $1, $2, $6, $5, $4, $3}' | \
awk '{print $1, $2, ($3 + $4 * 60 + $5 * 3600 + $6 * 86400)}'

1 init 31908525
...
1009 sendmail 10201331
1020 sendmail 10201330
...
29399 sshd 14493
29401 sshd 14491
...
29809 ps 0
...

That's more like it! So I've demonstrated that I can get to a list of PIDs, process names, and total seconds that the process has been running. I'm sure that if I thought about it some more, I could come up with a single awk statement to do what I'm doing with two statements above, but I think the above code is clearer and it wasn't really that hard to type.

But remember the original request was for a command to kill processes by name and date-time stamp, and not just output data for all processes. So our second awk statement is going to change anyway. Let's suppose that we wanted to kill all sshd processes that had been around for longer than 10 days. We could output the PIDs of the matching processes as follows:

# ps -eo pid,comm,etime | tail -n +2 | sed 's/[-:]/ /g' | \
awk '{print $1, $2, $6, $5, $4, $3}' | \
awk '($2 == "sshd") && (($3 + $4 * 60 + $5 * 3600 + $6 * 86400) > 864000) {print $1}'

5725

Other queries would be simpler. For example, let's output the PIDs of all sshd processes that have been active less than one day:

# ps -eo pid,comm,etime | tail -n +2 | sed 's/[-:]/ /g' | \
awk '{print $1, $2, $6, $5, $4, $3}' | \
awk '($2 == "sshd") && ($6 == "") {print $1}'

4727
4729
4805
4807
29399
29401

Here all we're doing is confirming that the sixth field is unset, which must mean that the process has been running less than one day. We don't need to do any math at all.

Anyway, now that we can select and output PIDs at will, the final solution is just putting the whole command in backticks and using it as an argument to the kill command:

# kill -9 `ps -eo pid,comm,etime | ...`

Whoosh! That sure was a lot of work for a simple request! I'm sort of shocked that Unix makes this so difficult. Could this be an opportunity for Tim to show me up with some Windows magic?

Tim opens Ed's mail

If "glory is fleeting, but obscurity is forever" (Napoleon) then that "fu" is going to live longer than either of us. Too bad Ed isn't here to bask in the glory of how easy this is in Windows. Of course, he is basking in the sun on vaction this week.

While Ed is gone, I like take a peak through his mail. Bills. Junk. More Bills. Victoria Secret catalog. A shipment of peanut butter, a stuffed water bufallo and some latex? Uh...Anyway, I did steal some of it too. Not the "other" stuff, but this easy episode.

Way back in episode 22, Ed killed process with wmic. This topic has been revisited a few times, including my favorite episode, Advanced Process Whack-a-Mole. If "wmic process" were a dead horse, we would have severely beaten it. We do have the new twist of searching based on the creation date, and it is pretty easy.

C:\> wmic process where (name="cmd.exe" AND creationdate ^< "20100511060000.000000-300") delete
The date format is yyyymmddhhmmss.mmmmmm-TTT. I have no idea what the -300 means Edit: Where the TTT is the timezone and it is required in the query. If you remove it you will get an Invalid Query error. In my case -300 represents my timezone (GMT -6).

Also, we have to escape any greater than or less than signs. The greater than and less than signs are used for redirection and the caret (^) character is used to escape it. I don't know how to make it sound more confusing, like Hal's section.

Tim opens his mail

This task is even easier in PowerShell, and it is pretty self explanatory, too.

C:\> Get-Process cmd | ? { $_.StartTime -lt "2010/5/11 6:00" } | Stop-Process
We can even try to find processes that have been running for longer than an hour.

C:\> Get-Process cmd | ? { $_.StartTime -lt (Get-Date).AddHours(-1) } | Stop-Process
In both cases, we use Get-Process to find processes named cmd. The next step is to filter based on the start time. Finally, we kill it.

Sorry Hal, for not making this portion totally unreadable and for not making this way more complicated that it should be. Got a bit of shell envy this week?

Signed, sealed, delivered.

Tuesday, May 4, 2010

Episode #93: Of Ports and Paths

Tim is sweating in Texas:

This week's episode is inspired by another one of our readers, Aaron Goad. He was working on a cool bit of fu to map out all of the executables that are listening for incoming network connections. Based on the information gathered, he hoped to create profiles for the different server types in a given environment. The data would be used to create histograms based on server types, and make it easy find one off processes that could be anything from a backup client to a netcat backdoor. He is planning on writing a paper on it for his SANS Gold Certification. Good luck Aaron. Aaron also sent us his command which was 99% of the way there, but there was one problem, which I'll explain later.

I'm in Texas this week visiting some family. It is only May, but dang it is hot. Ed is out this week on vacation, so I'm going to work overtime this week in this (literal) sweat shop. I'm hoping I'll get paid overtime too. Let's see $0 times 1.5 times...never mind.

We'll start off with the command in the classic Windows shell since that is what Aaron sent us. Here is my version of the command.

C:\> for /f "tokens=1,2,3,7 delims=: " %a in ('netstat -nao ^| find 
^"LISTENING^" ^| find /v ^"::^"') do @(for /f "tokens=1,*" %n in ('"wmic process
where processId=%d get caption,executablepath | find ".""') do @echo Protocol=%a,
IP=%b, Port=%c, PID=%d, Name=%n, Path=%o)


Protocol=TCP, IP=0.0.0.0, Port=135, PID=776, Name=svchost.exe,
Path=C:\Windows\system32\svchost.exe
Protocol=TCP, IP=0.0.0.0, Port=912, PID=2368, Name=vmware-authd.exe,
Path=C:\Program Files\VMware\VMware Player\vmware-authd.exe
Protocol=TCP, IP=0.0.0.0, Port=49153, PID=892, Name=svchost.exe,
Path=C:\Windows\System32\svchost.exe
Protocol=TCP, IP=0.0.0.0, Port=49154, PID=952, Name=svchost.exe,
Path=C:\Windows\system32\svchost.exe
Protocol=TCP, IP=0.0.0.0, Port=49155, PID=520, Name=lsass.exe,
Path=C:\Windows\system32\lsass.exe
Protocol=TCP, IP=0.0.0.0, Port=49157, PID=512, Name=services.exe,
Path=C:\Windows\system32\services.exe
...
I did cheat a little this week by filtering out all IPv6 addresses. All the extra colons really screw up our makeshift parser. IPv6 addresses are filtered out by removing all the lines containing "::" using the /v switch with find.

The cleaned up netstat output, which is just IPv4 listeners, is split using our For loop. Regular readers are well aware that there is no good way to parse text in the classic shell, so we have to use our good ol' For loop for this task (again). We use the delimiters colon and space to get the 1st, 2nd, 3rd, and 7th tokens which represent the protocol, local address, local port, and process id respectively.

Next, we need to use wmic to get the executable name and path. This is the part that caused the problems for Aaron. When wmic returns the properties, it sorts the properties alphabetically. The ExecutablePath property comes before the Name property. So what? Well, the path typically contains spaces which our parser uses as delimiters. There isn't a way to know how many spaces are in the path, so we don't know which variable will contain the Name property. The problem can be fixed by getting the name property first, but how? The Caption property contains the same value as the Name property and C comes before E. Problem solved. We can then use the 1st and *th tokens, where the 1st is the Caption and the *th contains the rest of the line (the Executable Path).

Now we have all the values we want:
%a tcp or udp
%b local ip
%c local port
%d is pid
%n is name
%o is executable path

With these variables we can dump them to a file or do what ever we want with them.

Tim's second shift, PowerShell

Unfortunately, PowerShell does not include a nice objectified version of netstat, so we will have to parse it ourselves. However, we do have regular expressions to help us parse.

Here is the command in PowerShell.

PS C:\> netstat -ano | 
? { $_ -match [regex]'\s+(?<Protocol>\S+)\s+(?<LocalAddress>(\[.*?\])|([0-9\.]+)):
(?<LocalPort>\d+).+LISTENING.+?(?<PID>\d+$)' } |
select @{Name="Protocol";Expression={$matches.Protocol}},
@{Name="LocalAddress";Expression={$matches.LocalAddress}},
@{Name="LocalPort";Expression={$matches.LocalPort}},
@{Name="Name";Expression={(Get-Process -id $matches.PID).Name}},
@{Name="Path";Expression={(Get-Process -id $matches.PID).Path}}


Protocol LocalAddress LocalPort Name Path
-------- ------------ --------- ---- ----
TCP 0.0.0.0 135 svchost C:\Windows\system32\svchost.exe
TCP 0.0.0.0 445 System
TCP 0.0.0.0 49154 svchost C:\Windows\system32\svchost.exe
TCP 0.0.0.0 49155 lsass C:\Windows\system32\lsass.exe
TCP 0.0.0.0 49157 services C:\Windows\system32\services.exe
TCP 192.168.70.1 139 System
TCP [::] 135 svchost C:\Windows\system32\svchost.exe
TCP [::] 445 System
TCP [::] 49152 wininit C:\Windows\system32\wininit.exe
TCP [::] 49157 services C:\Windows\system32\services.exe
TCP [::1] 49159 ccApp C:\Program Files\Common Files\...
...
This command looks really nasty, but it isn't too bad. It is just three portions.

netstat -ano | [regular expression] | [output cleanup]

The middle section uses a regular expression for filtering and for named groups (also called named captures or named capture groups). It will filter out lines that do not contain LISTENING so we are left with only listeners. The named capture groups will contain the protocol, local address, local port, and process id (pid). The syntax for a capture groups is (?<Name>Expression). The variable $matches contains the information for the named captures, and it can be used later in the command in our output.

Next, we then use select object and calculated properties to clean up the output into a nice object. The calculated properties, also called custom columns, are created using a hashtable. A hashtable is specified by @{ key1=value1, key2=value2, ... }. The hashtable for a calculated property uses the Name and Expression keys. Our first three custom columns are just our named captures from the regular expression. The remaining two columns require a bit more work. Inside the property expression we use Get-Process to retrieve the details for a process and then select the property we want, name and path.

It does take a little more work to get the command into a nice object, but it does make it easy to export or pipe into other commands.

So there is all the Windows fu for the week. Hal, whatcha got?

Hal is sweating a bit in Oregon too:

I have to admit at first I was feeling pretty cocky about this one. "Oh gee, I have to parse the output of several commands and produce a nice report? <sarcasm>That's really tough for us Unix folks!</sarcasm>"

The easy part was pulling the basic information together. I'm going to use my little friend "lsof -i" to dump information about network sockets on the system, using the "-n" (show IPs, not hostnames) and "-P" (show port numbers, not port names) options. A little awk fu will get us the PID, protocol, address, and port information for just the processes that are in "LISTEN" state:

# lsof -nP -i | awk '/LISTEN/ {print $2 " " $7 " " $8}'
...
4107 TCP *:902
4219 TCP *:8903
4219 TCP *:8902
...
18877 TCP 127.0.0.1:53
18877 TCP 10.66.1.2:53
18877 TCP 172.17.18.1:53
18877 TCP 172.17.17.1:53
18877 TCP 127.0.0.1:953
18877 TCP [::1]:953
...

I've edited the output here a bit in the interests of space, but I've left in a few representative entries that will turn out to be interesting in various ways.

Our first issue is splitting the port numbers from the IP addresses. As Tim points out, IPv6 addressing makes this a little more difficult than just splitting on colons. I decided to opt for a sed soltution:

# lsof -nP -i | awk '/LISTEN/ {print $2 " " $7 " " $8}' | sed -r 's/:([0-9]+)$/ \1/'
...
4107 TCP * 902
4219 TCP * 8903
4219 TCP * 8902
...
18877 TCP 127.0.0.1 53
18877 TCP 10.66.1.2 53
18877 TCP 172.17.18.1 53
18877 TCP 172.17.17.1 53
18877 TCP 127.0.0.1 953
18877 TCP [::1] 953
...

Here my sed expression is matching the last "colon followed by some digits" at the end of the line and replacing that with a space followed by those digits. This effectively removes the colon and inserts a space. A little ugly, but I'm not working up a sweat so far.

The next trick is getting the executable path. Unfortunately, this is where everything goes pear-shaped. My little friend lsof only outputs the base name of the command, and will even truncate the command name if it exceeds 9 characters, so that's no help. But then I recalled that the /proc file system contains the information we need:

# readlink /proc/4219/exe
/usr/lib/vmware/bin/vmware-hostd

The /proc file system features a /proc/<pid>/exe is a symlink that points to the executable file. But guess what? This is only a feature of the Linux /proc file system. Unfortunately, other Unix operating systems (e.g. Solaris) may not have this link. So I needed to come up with something more portable.

When in doubt, dip back into the lsof bag of tricks:

# lsof -a -p 4219 -d txt
COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME
vmware-ho 4219 root txt REG 253,2 49355280 230822 /usr/lib/vmware/bin/vmware-hostd

Here I'm using lsof to dump the files related to the "text segment" ("-d txt") for PID 4219 ("-p 4219"). The "-a" option does a logical "and" of the two conditions rather than "or" which is (rather oddly, IMHO) the default for lsof.

As you can see, on my Linux system the output is a header line plus a line that describes the executable. On other Unix architectures, however, you may also get a bunch of additional lines that describe all of the shared libraries required by the executable. The good news is that the actual executable is always listed first. So the next trick is to extract the last field from the first line after the header:

# lsof -a -p 4219 -d txt | awk '/txt/ {print $NF}' | head -1
/usr/lib/vmware/bin/vmware-hostd

Here I'm matching on the string "txt" in the non-header lines and dumping the last field with $NF. I then use head to make sure I only get the first non-header line just in case there are multiple lines of output.

Looking good so far, but check out this interesting example:

# lsof -a -p 4107 -d txt | awk '/txt/ {print $NF}' | head -1
(deleted)
# lsof -a -p 4107 -d txt
COMMAND ... NAME
vmware-au ... /usr/sbin/vmware-authdlauncher.#prelink#.gvYLje (deleted)

Here I've edited out the middle columns of output from the second command so you can more clearly see what's going on. Our hero VMware is running an executable that was subsequently deleted. Because our first command using $NF to dump out the last field delimited by whitespace, we just get the "(deleted)" bit. The work-around is to explicitly dump the 9th column (the executable path) and then the 10th column (the "deleted" marker) if it exists:

# lsof -a -p 4107 -d txt | awk '/txt/ {print $9 " " $10}' | head -1
/usr/sbin/vmware-authdlauncher.#prelink#.gvYLje (deleted)
# lsof -a -p 4219 -d txt | awk '/txt/ {print $9 " " $10}' | head -1
/usr/lib/vmware/bin/vmware-hostd

Whew! With me so far? We're in the home stretch now. All we have to do is take our initial lsof pipeline that outputs PID, protocol, IP, and port and combine that with our hack to recover the executable names:

# lsof -nP -i | awk '/LISTEN/ {print $2 " " $7 " " $8}' | sed -r 's/:([0-9]+)$/ \1/' | \
while read pid rest; do
echo "$rest" `lsof -a -p $pid -d txt | awk '/txt/ {print $9 " " $10}' | head -1`;
done

...
TCP * 902 /usr/sbin/vmware-authdlauncher.#prelink#.gvYLje (deleted)
TCP * 8903 /usr/lib/vmware/bin/vmware-hostd
TCP * 8902 /usr/lib/vmware/bin/vmware-hostd
...
TCP 127.0.0.1 53 /usr/local/depot/bind/9.6.1-P1/sbin/named
TCP 10.66.1.2 53 /usr/local/depot/bind/9.6.1-P1/sbin/named
TCP 172.17.18.1 53 /usr/local/depot/bind/9.6.1-P1/sbin/named
TCP 172.17.17.1 53 /usr/local/depot/bind/9.6.1-P1/sbin/named
TCP 127.0.0.1 953 /usr/local/depot/bind/9.6.1-P1/sbin/named
TCP [::1] 953 /usr/local/depot/bind/9.6.1-P1/sbin/named
...

This looks pretty fugly, but it's actually quite simple. We're using a while loop to read the output of our first lsof command line-by-line. We pull the PID out of the first field of each line and then save the rest in $rest. The only statement inside the while loop simply echoes $rest followed by the executable path name extracted by our crazy lsof concoction.

Alert readers may note that my echo statement includes quotes around $rest. Why did I do that? Well remember that in many cases in our output the IP address appears as "*". If we just did "echo $rest" without quotes around $rest, then the "*" would actually be interpolated as a shell glob and we'd end up echoing the contents of whatever directory we were in when we ran the command. This is definitely not what we want!

I can't say that I'm overly happy with the amount of code I needed to sling around to solve this week's puzzle. The Linux-specific solution that uses readlink is much cleaner, but I'll leave that one as an exercise to the reader.