Using Python to Make Windows Port Mapping Useful

Posted:05/07/2015 12:00PM

Using Python to Make Windows Port Mapping Useful

Mike Mclain discusses how to use python to make Windows port mapping more useful

Preface:

Last night (as i was perusing reddit) my internet connectivity inexplicably stopped. While i am no stranger to identifying and debugging network problems, this particular occurrence peeked my curiosity because of my sudden inability to access local or LAN addresses (like 127.0.0.1 or 192.168.1.1 respectively), which is the hallmark of a PC related issue (like driver problems, PC hardware problems, virus, et cetera).

The Problem:

Conversely, upon regaining network and internet connectivity (noting that everything went back to normal after around five minutes), i decided to snoop around in my ASUS RT-AC68R router's system logs in order to try and determine if something nefarious was aloof or if my PC just happened to encounter a natural bug / buffer overload.

Likewise, after examining the system logs and finding a few active connections that felt somewhat suspicious, i was left with the task of trying to track down the source of these active connections on my PC when i hit a bit of a snafu in resolving the network connections to their corresponding application name.

To elaborate further, the most common method of examining active network connections on a Window's PC (particular for a Windows Vista or Windows 7 operating system) is to either utilize the GUI Task Manager Resource Monitor or the Terminal (CMD.exe) command netstat -aon. Conversely, while the GUI approach will not be examined further, the execution of the netstat -aon command yields a console output similar to:

Active Connections

Proto   Local Address    Foreign Address   State       PID
TCP     0.0.0.0:80       0.0.0.0:0         LISTENING   14232
TCP     0.0.0.0:81       0.0.0.0:0         LISTENING   1836
TCP     0.0.0.0:135      0.0.0.0:0         LISTENING   388
TCP     0.0.0.0:443      0.0.0.0:0         LISTENING   14232
TCP     0.0.0.0:444      0.0.0.0:0         LISTENING   1836
TCP     0.0.0.0:445      0.0.0.0:0         LISTENING   4
TCP     0.0.0.0:554      0.0.0.0:0         LISTENING   5504
TCP       0.0.0.0:1170     0.0.0.0:0         LISTENING   7124
TCP       0.0.0.0:2869     0.0.0.0:0         LISTENING   4
TCP       0.0.0.0:2968     0.0.0.0:0         LISTENING   5936
TCP       0.0.0.0:4242     0.0.0.0:0         LISTENING   2092
TCP       0.0.0.0:5357     0.0.0.0:0         LISTENING   4
TCP       0.0.0.0:6800     0.0.0.0:0         LISTENING   8020
TCP       0.0.0.0:6802     0.0.0.0:0         LISTENING   8020
TCP       0.0.0.0:10243    0.0.0.0:0         LISTENING   4
TCP       0.0.0.0:27036    0.0.0.0:0         LISTENING   19484

and both approaches only provide the application's Process ID (PID), which is not immediately helpful in determining if a connection is malicious or not without further investigation.

Likewise, in order to aid in this determination, either the GUI Task Manager Resource Monitor (which will not be discussed) or the Terminal (CMD.exe) command tasklist /v (which produces output similar to:

Image Name          PID         Session Name      Session#    Mem Usage   Status      User Name               CPU Time          Window Title
=================================================================================================================================================
System Idle Process  0          Services          0           24 K        Unknown     NT AUTHORITY\SYSTEM     1401:41:10        N/A 
System               4          Services          0           23,556 K    Unknown     N/A                     9:45:53           N/A 
smss.exe             328        Services          0           276 K       Unknown     N/A                     0:00:00           N/A 
csrss.exe            592        Services          0           3,388 K     Unknown     N/A                     0:01:33           N/A 
csrss.exe            668        Console           1           43,248 K    Running     N/A                     0:33:06           N/A 
wininit.exe          676        Services          0           764 K       Unknown     N/A                     0:00:00           N/A 
winlogon.exe         732        Console           1           2,600 K     Unknown     N/A                     0:00:00           N/A 
services.exe         780        Services          0           9,684 K     Unknown     N/A                     0:01:03           N/A 
lsass.exe            788        Services          0           11,164 K    Unknown     N/A                     0:09:42           N/A 
lsm.exe              796        Services          0           2,464 K     Unknown     N/A                     0:00:12           N/A 
svchost.exe          900        Services          0           7,488 K     Unknown     N/A                     2:18:17           N/A 
nvvsvc.exe           984        Services          0           6,268 K     Unknown     N/A                     0:00:03           N/A 
nvSCPAPISvr.exe      1008       Services          0           2,024 K     Unknown     N/A                     0:09:28           N/A 
svchost.exe          388        Services          0           8,612 K     Unknown     N/A                     0:00:56           N/A

) must be utilized to associate the obtained PID (via netstat) to an executable name. While this approach is not particularly difficult (noting that grepers can perform some plumbing and obtain meaningful results with some success); however, such complexities feel overly cryptic given the fundamental usefulness and general application of this particular task (a notion that is supported by the innate improvements found in the Windows 8 Task Manager).

The Solution:

Nevertheless, such inherent difficulties (in performing this relatively straightforward task) annoyed me to the point of writing a python script that would execute and process the results from the netstat -aon command and then associate the PID obtained with its corresponding executable name (via executing and processing the tasklist /v command), and then returning the result obtained in an easy to read format (similar to:

Port  RemoteIP  PID     Executable Name         User           Title
=====================================================================
80    0.0.0.0   14232   Skype.exe               Desktop\User   N/A
81    0.0.0.0   1836    httpd.exe               N/A            N/A
135   0.0.0.0   388     svchost.exe             N/A            N/A
443   0.0.0.0   14232   Skype.exe               Desktop\User   N/A
444   0.0.0.0   1836    httpd.exe               N/A            N/A
445   0.0.0.0   4       System                  N/A            N/A
554   0.0.0.0   5504    wmpnetwk.exe            N/A            N/A
1170  0.0.0.0   7124    PlexDlnaServer.exe      Desktop\User   N/A
2869  0.0.0.0   4       System                  N/A            N/A
2968  0.0.0.0   5936    EEventManager.exe       Desktop\User   Epson Event Manager Background
4242  0.0.0.0   2092    CrashPlanService.exe    N/A            N/A
5357  0.0.0.0   4       System                  N/A            N/A

).

Likewise, in order to obtain this objective, the following python code was created:

# -*- coding: utf-8 -*-
""" A Simplistic Python Script to map the results obtained from the netstat
command with the application running them.
"""
# Used to run console commands from python
from subprocess import check_output

# Used to create regular expressions
import re


def id_netstat_processes():
    """ A Simplistic Python Script to map the results obtained from the netstat
    command with the application running them.
    """

    # First Run netstat to get network connections
    # Options
    # a Displays all active TCP connections and the TCP and UDP ports on which
    #   the computer is listening.
    # o Displays active TCP connections and includes the process ID (PID) for each
    #   connection. You can find the application based on the PID on the Processes
    #   tab inWindows Task Manager. This parameter can be combined with -a, -n, and -p.
    # n Displays active TCP connections, however, addresses and port numbers are
    #   expressed numerically and no attempt is made to determine names.
    result = check_output("netstat -aon", shell=True)

    # Now make an array of terms to remove from the data obtained from netstat 
    clean_up_array = [
        ("Active Connections", ""),
        ("Proto", ""),
        ("Local Address", ""),
        ("Foreign Address", ""),
        ("State", ""),
        ("PID", ""),
        ("\r", ""),
        ("\t", " ")
    ]

    # Remove terms from the netstat data
    for find, replace in clean_up_array:
        result = result.replace(find, replace)

    # * Because I am feeling feisty, I will utilize an evil regular expression to extract the
    #   netstat information via regex groups via the () command
    #   
    # * Likewise Because terminal output is space padded rather than tabbed we need to account
    #   for variable spacing via the regex (space)* or ' *' expression
    #   
    # * Becuase the 1st group is either UDP or TCP use (UDP|TCP) to find either or
    #   
    # * Because the 2ed group is either a IPV4 xxx.xxx.xxx.xxx or IPV6 XXXX::XXXX::XXXX::XXXX::XXXX%xx address or [::]
    #   Use [0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]* for any or none number length with dots or
    #   [ *[a-z0-9]*:* *[a-z0-9]*:* *[a-z0-9]*:* *[a-z0-9]*:* *[a-z0-9%]*\\] for any or none letters or numbers of : until the port :
    #   
    # * Because the 3ed group is the port use any number [0-9]*
    # 
    # * Because the 4th group is remote ip (i only had ipv4) or *:* use [0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*|\\[:*\\] or \\*
    # 
    # * Because the 5th group is the remote ip use any number [0-9]* 
    # 
    # * Because the 6th group is the status use LISTENING|ESTABLISHED|TIME_WAIT|CLOSE_WAIT with any or none group find
    # 
    # * Becuase the 7th group is pid use any number [0-9]* 
    # a ugly regex string that extracts the required information into groups... does not support IPV6 remote address atm but supports local IPV6
    reexstring = " *(UDP|TCP) *([0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*|\\[ *[a-z0-9]*:* *[a-z0-9]*:* *[a-z0-9]*:* *[a-z0-9]*:* *[a-z0-9%]*\\]):([0-9]*) *([0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*|\\[:*\\]|\\*):(\\*|[0-9]*) *(LISTENING|ESTABLISHED|TIME_WAIT|CLOSE_WAIT)* *([0-9]*)"

    # Build the regex string
    regexcompiled = re.compile(reexstring)

    # Process the input
    items = regexcompiled.finditer(result)

    networkitems = []

    # Loop thru the results
    for match in items:
        # we could just do data.append((match.groups())) but do this for user Configurability
        # Extract and trim the data obtained
        networktype = match.group(1).strip()
        localip = match.group(2).strip()
        localport = match.group(3).strip()
        remoteip = match.group(4).strip()
        remoteport = match.group(5).strip()

        # Because status can be None we need to check for None
        if not match.group(6) is None:
            status = match.group(6).strip()
        else:
            status = ""
        pid = match.group(7).strip()

        # Append items to an array for future processing
        networkitems.append(( localip, localport, remoteip, remoteport, status, pid))

    # At this point we are ready to get a list of all PID running
    tasklist = check_output("tasklist /v", shell=True)

    # Again our console result needs to be cleaned up prior to processing 
    clean_up_array = [
        ("Image Name", "" ),
        ("PID", "" ),
        ("Session Name", "" ),
        ("Session#", "" ),
        ("Mem Usage", "" ),
        ("Status", "" ),
        ("User Name", "" ),
        ("CPU Time", "" ),
        ("Window Title", "" ),
        ("=",""),
        ("\r", ""),
        ("\t", " ")
    ]

    # Remove terms from the tasklist data
    for find, replace in clean_up_array:
        tasklist = tasklist.replace(find, replace)


    # Because application names can have spaces in them , our regex becomes a touch more complex and requires look aheads via ?
    # Likewise, because the terminal is heavily space padded we can safely assuming two spaces will end each segment
    # thus, '  *' (or space space *) and '   *' (space space space *) are used in the look ahead as group stoppers
    # Beyond this, wildcard (.*) for any characters are used for group extraction
    regexstring2 = "^(.*?)   *([0-9]*) *(.*?)  *([0-9]*) *([0-9,]* .) *(.*?)  *(.*?)   *([0-9:]*) *(.*?)  "

    # To make life easier, a dictionary will be utilized to lookup the pid
    tasks = {}

    # Build the regex string And allow for multiline processing
    regexcompiled2 = re.compile(regexstring2, re.MULTILINE)

    # Process the input
    items = regexcompiled2.finditer(tasklist)

    # Loop thru the results
    for match in items:
        # Extract and trim the data obtained
        imagename = match.group(1).strip()
        pid = match.group(2).strip()

        # Sometimes this approach yields an empty string at the start check for this and continue if found
        if pid == '':
            continue

        sessionname = match.group(3).strip()
        sessionnumber = match.group(4).strip()
        memory = match.group(5).strip()
        status = match.group(6).strip()
        user = match.group(7).strip()
        cputime = match.group(8).strip()
        title = match.group(9).strip()

        # Populate our dictionary with information
        tasks[pid] = (imagename,sessionname,sessionnumber,memory,status,user,cputime,title)

    # Create a variable to hold our output
    output = ""
    # Loop thru all netstat items
    for item in networkitems:
        # Extract our array object
        localip, localport, remoteip, remoteport, status, pid = item

        # See if the PID exists within our PID array
        if pid in tasks.keys():
            # If so extract the PID information and add it to the output
            imagename,sessionname,sessionnumber,memory,status,user,cputime,title = tasks[pid]
            output += localport.ljust(10) + remoteip.ljust(20) + pid.ljust(10) + imagename.ljust(35)  + user.ljust(35) + title + "\n"
        else:
            # If not report the error in the output, if this happens then the application is likely something hidden deep in
            # administrative privileges and googling will be required to attempt to access it. This is the domain of viruses!
            output += localport.ljust(10) + remoteip.ljust(20) + "PID "+ pid +" Missing" + "\n"

    # Dump our output to a file 
    temp = open("netstat.txt", 'w')
    temp.write(output)
    temp.close()


# This is our application entry point
if __name__ == "__main__":
    # Run our extraction function
    id_netstat_processes()

and such code can be easily modified to meet the demands of your particular application or used as is.

Conclusion:

While i am personally amazed that such functionality is not standard (at least within Windows Vista or Windows 7), utilizing python to obtained such results was a relatively straightforward task. Likewise, such approaches can be extended further, for example, through the utilization of the getopt class (in order to add terminal command support for file saving or output formatting), or compiled into a standalone executable (for implementation into a executable terminal command ) via the py2exe package, if so desired.

Enjoy!

Comments:

comments powered by Disqus