Friday, November 02, 2007

Powershell: IP Packet Sniffer Script

I was looking for a Powershell script that would capture raw IP packets on the network and shove them into an object, but the only one I was able to find was a commercial cmdlet that was out of my budget. :-) So, I decided that I would attempt to write my own. I figured it would be a great learning exercise (and it was), but I went into the project with the goal of avoiding any 3rd party software, to avoid compiling anything (like a cmdlet written in C#), and I didn't want to install a driver shim. In other words, I wanted a plain 'ol script that could be run on any computer.

I basically spent a lot of time googling and I got a lot of help from the guys over in the #Powershell IRC channel (irc.freenode.net). All of that combined with trial-and-error, I give you.... *drumroll* get-packet.ps1.

The script recognizes IPv4 TCP, UDP, ICMP, and IGMP packets (for now). The thing that took me the longest amount of time trying to figure out was how to get all packets that the network card was seeing on the wire, not just packets destined for my IP address and a particular port number. The solution was hard to find but wasn't terribly difficult to understand.


# Create a new socket... SocketType should be Raw, and ProtocolType must be IP for promiscuous mode.
$socket = new-object system.net.sockets.socket([Net.Sockets.AddressFamily]::InterNetwork,[Net.Sockets.SocketType]::Raw,[Net.Sockets.ProtocolType]::IP)

# Include the IP header so we get the full packet
$socket.setsocketoption("IP","HeaderIncluded",$true)

# bind to a local IP address
$ipendpoint = new-object system.net.ipendpoint([net.ipaddress]"$localIP",0)
$socket.bind($ipendpoint)

# this switches the NIC driver into promiscuous mode. This requires admin rights.
[void]$socket.iocontrol([net.sockets.iocontrolcode]::ReceiveAll,$byteIn,$byteOut)


I read somewhere that .net sockets really just uses winsock under the hood, so it really helped my understanding to read both the winsock and dotnet documentation regarding sockets on msdn. I won't bother repeating what that documentation says here but if you're trying to decypher this script and can't quite figure it out, feel free to ask questions and I'll try to explain.

Something else that I probably knew at one point but had since forgotten was that the byte order on the network wire is reversed. On the wire it is "Big Endian" and on the PC it is "Little Endian." Wikipedia has a great explanation on Endianness. Figuring out how to interpret the IP packets was the next biggest time suck and endianness was a large part of it. Once I realized that NetworkToHostOrder didn't support unsigned ints, I simply wrote a few functions to reverse the byte order (from "Network" to "Host") and used BitConverter to return the correct data type.


# Takes a 2 byte array, switches it from big endian to little endian, and converts it to uint16.
function NetworkToHostUInt16 ($value)
{
[Array]::Reverse($value)
[BitConverter]::ToUInt16($value,0)
}

# Takes a 4 byte array, switches it from big endian to little endian, and converts it to uint32.
function NetworkToHostUInt32 ($value)
{
[Array]::Reverse($value)
[BitConverter]::ToUInt32($value,0)
}

# Takes a byte array, switches it from big endian to little endian, and converts it to a string.
function ByteToString ($value)
{
$AsciiEncoding = new-object system.text.asciiencoding
$AsciiEncoding.GetString($value)
}


After getting all the data out of the packets, I shove the ones I really want into a psobject. When running the script, it will be visually more appealing if you pipe the output to format-table.


PS C:\> ./get-packet.ps1 | ft


There are a lot more protocol types that I could be looking for, and I'll probably add some of the more common ones eventually. I've also looked into adding support to export and import the libpcap file format, but after looking at the libpcap source code, there's a lot of ugly in there that I might just avoid for now. :-)

If you find any bugs or have any ideas on optimizing the code, let me know!

Enjoy! :-)

- Robbie

Thursday, April 05, 2007

Powershell: Send email without installing the SMTP service

This is a sample script that shows how to send an email from PowerShell without installing the SMTP service on the local system. There is a VBScript example similar to this in the Technet script repository, but it wasn't too hard to convert it to Powershell.


$email = new-object -comobject "cdo.message"
$email.From = "sample@domain.com"
$email.To = "sample@domain.com"
$email.Subject = "App Errors"
$email.TextBody = "The app encountered the following errors: `n`n"

foreach ($f in get-content app.log | select-string "does not exist")
{
$email.TextBody += "$f `n"
}

$email.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2
$email.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserver") = "smtp.sample.com"
$email.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25

$email.Configuration.Fields.Update()
$email.Send()

Thursday, March 15, 2007

Powershell: Return value from remote registry key

One of the first things I wanted to do in Powershell was to get a value from a registry key on remote servers and show it to me in a list. For example, we use a backup program called Tivoli Storage Manager. If I want to see what version of the TSM client I have installed on all of my managed servers, here is a script that will do it.

A few things to note about this script. I'm using another function to get the list of servers that I want to check (see my previous blog post for more information). This script will actually ping each server before running get-wmiobject because get-wmiobject has a long timeout, and I dont want to have to wait 60 seconds for every server that may not be on the network.


$ErrorActionPreference = "silentlycontinue"
$starttime = get-date
$HKLM = 2147483650

$servers = get-servers

$auth = get-credential admin

# display the header info
write-host $('{0,-17}{1}' -f "ServerName","TSM Version")
write-host "--------------- -----------"

$count = 0

# sort the computer names and get various wmi info for that server
$servers.keys | sort | foreach `
{
$ping = get-wmiobject win32_pingstatus -filter "address='$_'"

if ($ping.statuscode -eq 0)
{
if ($servers.$_ -eq "ad" )
{
$reg = get-wmiobject -list -namespace root\default -computer $_ | where-object {$_.name -eq "StdRegProv" }

if ($? -eq $false)
{
write-host $_.tolower() "...wmi query failed." -foregroundcolor Red
}
else
{
$tsmver = $reg.getstringvalue($HKLM, "software\ibm\adsm\currentversion\backupclient","ptflevel")

write-host $('{0,-17}{1}' -f $_.tolower(),$tsmver.svalue)
}
}
else
{
$reg = get-wmiobject -list -namespace root\default -computer $_ -credential $auth

if ($? -eq $false)
{
write-host $_.tolower() "...wmi query failed." -foregroundcolor Red
}
else
{
$tsmver = $reg.getstringvalue($HKLM, "software\ibm\adsm\currentversion\backupclient","ptflevel")

write-host $('{0,-17}{1}' -f $_.tolower(),$tsmver.svalue)
}
}
}
else
{
write-host $_.tolower() "...ping failed." -foregroundcolor Red
}

$count += 1
}

write-host
write-host "Total servers: $count"
$endtime = (get-date).subtract($starttime)
write-host "Elapsed time:" $('{0:D2}:{1:D2}:{2:D2}' -f $endtime.hours,$endtime.minutes,$endtime.seconds)
write-host

Powershell: Find all managed servers

Where I work, about half of the servers I manage are active directory member servers, and the other half are standalone servers. Eventually they will all be in AD, but for now, thats just the way things are. Anyway, it has obviously been a systems management problem for running remote scripts. Now that Powershell is finally out, I decided to try and write some scripts to make my life easier. I decided to start blogging about some of what I’ve done because it is hard to find sample Powershell scripts, and the books that are currently out don’t quite go in depth enough.

So this brings me to my first Powershell function called get-servers.

get-servers will search an Active Directory OU (or more) for all computer objects, store it in a hashtable. In addition, it reads a text file (in the current directory) and adds those servers to the same hashtable. Server names are stored as keys, and “ad” or “standalone” is stored as the value. This way, I can use simple if/else statements to determine whether or not I need to pass along alternate credentials when using cmdlets such as get-wmiobject.


function get-servers()
{
$ErrorActionPreference = "silentlycontinue"
$objComputer = @()
$strCategory = "computer"
$servers = @{}

# bind to the directory - this also sets our starting point for the search
$objDomain = [ADSI]'LDAP://OU=myou,DC=mydomain,DC=com'
$objDomainDC = [ADSI]'LDAP://OU=Domain Controllers,DC=mydomain,DC=com'

$objSearcher = new-object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = $objDomain
$objSearcher.Filter = ("(objectCategory=$strCategory)")
[void]$objSearcher.PropertiesToLoad.Add("name")

# run the search
$colResults = $objSearcher.FindAll()

$objSearcher.SearchRoot = $objDomainDC
$colResults += $objSearcher.FindAll()

$colResults | foreach-object { $servers.$($_.properties.name) = "ad" }

# add standalone servers
get-content servers.txt | foreach { $servers.$_ = "standalone" }

return $servers
}

Edit and save this to a file such as “get-servers-function.ps1″ and load it into your current environment by running:

 . ./get-servers-function.ps1

Then if you run get-servers then it should return your servers as designed. The advantage to having this in a function is so that you don’t have to duplicate the code in every script that you write. All you have to do when writing a new script is just add this line near the top:

 $servers = get-servers

Enjoy!