Reassembling Packets with the Network Monitor API

Network traffic by nature is fragmented. Limits of various network packet sizes force protocols to chop up data into multiple frames. When you capture data or read it from a trace with the API (NMAPI) you see only the fragments by default. But as the engine is collecting packets, it can be configured to pass up the reassembled payloads as well. For an intro to how assembly works in the UI, please see the video on reassembly. We also released a recent video on Channel 9 which has some information about the API and reassembly. I would also recommend reading the "Introduction to the Network Monitor API" in the help file for a general background.

Configuring the Parser

The first step is to configure your parser to reassemble. Your API tool for breaking apart a frame is called the Frame Parser object. But to create a frame parser, you start by creating a Frame Parser Configuration. This configuration allows you to add data fields and properties. But it also allows you configure your parser for Reassembly and Conversations. In this case Reassembly might depend on Conversations, so we will enable them both. Here's how I setup my Parser Configuration and Frame Parser.

 // Returns a frame parser with a filter and one data field.
// INVALID_HANDLE_VALUE indicates failure.
HANDLE
MyLoadNPL(void)
{
    HANDLE myFrameParser = INVALID_HANDLE_VALUE;
    ULONG ret;

    // Use NULL to load default NPL set.
    ret = NmLoadNplParser(NULL, NmAppendRegisteredNplSets, MyParserBuild, 0, &g_NplParser);

    if(ret == ERROR_SUCCESS){
        ret = NmCreateFrameParserConfiguration(g_NplParser, MyParserBuild, 0, &g_FrameParserConfig);

        if(ret == ERROR_SUCCESS)
        {
            // Order is important here, must turn on Conversations before Reasembly.
            ret = NmConfigConversation(g_FrameParserConfig, NmConversationOptionNone , TRUE);
            if(ret != ERROR_SUCCESS)
            {
                wprintf(L"Failed to config reassembly, error 0x%X\n", ret);
            }

            ret = NmConfigReassembly(g_FrameParserConfig, NmReassemblyOptionNone , TRUE);
            if(ret != ERROR_SUCCESS)
            {
                wprintf(L"Failed to config reassembly, error 0x%X\n", ret);
            }

            // Property so we can show the highest protocol description.
            ret = NmAddProperty(g_FrameParserConfig, L"property.Description", &g_DescPropID);
            if(ret != ERROR_SUCCESS)
            {
                wprintf(L"Failed to add field, error 0x%X\n", ret);
            }

            ret = NmCreateFrameParser(g_FrameParserConfig, &myFrameParser, NmParserOptimizeNone);

            if(ret != ERROR_SUCCESS)
            {
                wprintf(L"Failed to create frame parser, error 0x%X\n", ret);
                NmCloseHandle(g_FrameParserConfig);
                NmCloseHandle(g_NplParser);
                return INVALID_HANDLE_VALUE;
            }
        }
        else
        {
            wprintf(L"Unable to load parser config, error 0x%X\n", ret);
            NmCloseHandle(g_NplParser);
            return INVALID_HANDLE_VALUE;
        }

    }
    else
    {
        wprintf(L"Unable to load NPL\n");
        return INVALID_HANDLE_VALUE;
    }

    return(myFrameParser);
}

After creating your Frame Parser Configuration Object, you'll want to set any options first. This will let the engine optimize properly when adding other things like properties and data fields. It's also important that you turn on conversations before reassembly. Placing them in the wrong order will turn off Reassembly due to a bug in our API.

Above we also added a property so that I can show the description of the current frame. This is not necessary for reassembly to work, but it helps us understand the example.

Parsing the Frames

It is up to the parsers (NPL) to mark each frames fragment type: First=1, Middle=2, Last=3 or None=0. The engine tracks these fragments and returns a new inserted raw frame once a Last fragment is detected for a specific protocol.

When you parse a raw frame using NmParseFrame, the last parameter passed is a pointer to a HANDLE that will contain an InsertedRawFrame if one is present. Otherwise this value will be set to INVALID_HANDLE_VALUE for any frame that doesn't return a reassembled payload. For frames that do have a reassembled payload, the handle returned will contain a raw frame. You can now use your frame parser to parse this raw frame.

The main part of my code simply retrieves frames from the capture file iteratively and calls ParseFrame, which does all the work. If an inserted frame is found, the function calls itself. The function is recursive because the handles for a RawFrame, ParsedFrame and InsertedRawFrame have to be closed in the order they were opened. There are other ways to do this, but for this example a recursive routine was the easiest. You will also want to insure the frames are in order. For instance you could use NmOpenCaptureFileInOrder to make sure the TCP frames are ordered correctly.

In my case I parse and display all the frames so that you can get a feel for the pattern that occurs as frames fragments are marked by the engine. It also helps to shows how fragmentation looks at different protocol layers. If you were interested in only the reassembled frames or frames that are not fragmented to begin with, you could identify those as having a fragment type of None and no InsertedRawFrame.

Here's the recursive frame parsing routine:

 // Recursive Parsing routine.  If an inserted frame is found, the recusive routine is called again.  This
// allows us to close our handles in the order there were created.
void
MyParseFrame(HANDLE frameParser, HANDLE rawFrame, ULONG curFrame, PULONG reassembleFrames, int reassembleCount)
{
    ULONG ret;
    HANDLE ParsedFrame = INVALID_HANDLE_VALUE;
    HANDLE InsRawFrame = INVALID_HANDLE_VALUE;

    // NmUseFrameNumber and valid unique frame numbers are neccessary for Reassembly to work properly.
    ret = NmParseFrame(frameParser, rawFrame, curFrame + *reassembleFrames, NmFieldDisplayStringRequired | NmUseFrameNumberParameter, &ParsedFrame, &InsRawFrame); 
    if(ret == ERROR_SUCCESS)
    {
        // Returns the highest level protocol description just to show which
        // frame we are working on.
        PBYTE buf = GetDescription(frameParser);

        // Get the fragment information which helps understand what is happening,
        // but not needed for reassembly to work.
        NM_FRAGMENTATION_INFO FragInfo;
        GetFragType(ParsedFrame, &FragInfo);

        wprintf(L"%5d-%d: %5d      %-5.5s-%d    %-.45s\n", curFrame+1, reassembleCount, curFrame+(*reassembleFrames)+1, FragInfo.FragmentedProtocolName, FragInfo.FragmentType, buf);

        free(buf);

        if(InsRawFrame != INVALID_HANDLE_VALUE)
        {
            (*reassembleFrames)++;
            MyParseFrame(frameParser, InsRawFrame, curFrame, reassembleFrames, reassembleCount+1);

            NmCloseHandle(InsRawFrame);
        }
    }

    NmCloseHandle(ParsedFrame);
    NmCloseHandle(InsRawFrame);
}

When doing reassembly you must add the Frame Number parameter. It must also be unique, so you have to remember to increment when adding and parsing the reassembled frames. The GetFragType uses NmGetFrameFragmentInfo API call to determine the fragment type and protocol. You can look at the full example below to see how it works in details, but those ancillary pieces are pretty straight forward.

Looking at an Example

Below is the partial output for an example capture. In my notation, the Frame# contains a number after the dash that shows when multiple iterations occur on a frame. The Reassem# is the frame number that would appear in a reassembled trace in the UI and is what is used to seed each frame with a unique frame number.

 Frame# Reassem# FragType Description

5-0: 5 TCP -1 HTTP:Response, HTTP/1.1, Status: Bad gateway,

6-0: 6 TCP -2 TCP:[Continuation to #5]Flags=...A...., SrcPo

7-0: 7 -0 TCP:Flags=...A...., SrcPort=49382, DstPort=HT

8-0: 8 TCP -3 TCP:[Continuation to #5]Flags=...AP..., SrcPo

8-1: 9 -0 HTTP:Response, HTTP/1.1, Status: Bad gateway,

...

In original frames 5-8, you can see a typical TCP fragmentation. Frame 5 is a TCP First fragment. Frame 6 is a middle fragment and frame 7 is traveling in the opposite direction so it's not part of this reassembly stream. Frame 8 is the last frame in the reassembled TCP payload which is marked as the Last fragment. This is where the Inserted Raw Frame is valid and the recursive call to parse the frame would occur. Frame 8-1, is the parsed inserted frame which you can see matches the description of frame #5, but if you looked at it, there would be two differences.

First, since it's an inserted frame it will have a PayloadHeader structure as its top protocol. This is a protocol we manufactured to take the place of the carrying protocol, in this case TCP. Having a duplicate TCP frame would confuse our parsers and perhaps the user as well. So this header takes it place and calls HTTP directly.

Second, this frame will have a larger payload. It will consist of all the payload data from frame 5, 6, and 8.

Two Level Reassembly

In this next example, both TCP and HTTP has fragmented data.

 ...

33-0: 36 TCP -1 HTTP:Response, HTTP/1.1, Status: Ok, URL: htt

34-0: 37 TCP -2 TCP:[Continuation to #36]Flags=...A...., SrcP

35-0: 38 -0 TCP:Flags=...A...., SrcPort=49384, DstPort=HT

36-0: 39 TCP -3 TCP:[Continuation to #36]Flags=...AP..., SrcP

36-1: 40 HTTP -1 HTTP:Response, HTTP/1.1, Status: Ok, URL: htt

37-0: 41 TCP -1 HTTP:HTTP Payload, URL: https://www.google.com

38-0: 42 -0 TCP:Flags=...A...., SrcPort=49384, DstPort=HT

39-0: 43 TCP -2 TCP:[Continuation to #41]Flags=...A...., SrcP

40-0: 44 TCP -2 TCP:[Continuation to #41]Flags=...A...., SrcP

41-0: 45 -0 TCP:Flags=...A...., SrcPort=49384, DstPort=HT

42-0: 46 TCP -3 TCP:[Continuation to #41]Flags=...AP..., SrcP

42-1: 47 HTTP -3 HTTP:HTTP Payload, URL: https://www.google.com

42-2: 48 -0 HTTP:Response, HTTP/1.1, Status: Ok, URL: htt

...

Frames 33-36 make up the first HTTP fragment. As you can see, the inserted frame at 36-1 is a First fragment, but the protocol is now HTTP. Frames 37-42 make up the next HTTP fragment which is inserted at frame 42-1. This inserted frame is the HTTP Last fragment so now there is yet another inserted raw frame that we must iterate through and parse. Frame 42-2 is the final reassembled frame and contains the original HTTP Response in its entirety. The description matches frame 33 because it the data starts with payload in that frame but it also includes the payloads from frames 34, 36, 37, 39, 40, and 42. However, from the engines point of view, it really collects the payloads from frame 36-1 and 42-1. But each of these is made up from the fragmented frames mentioned above.

The Whole Shebang

Below I've placed the entire source code for the example described in this blog. While it depends on which protocols you are interested in, having access to the reassembled data can provide you with the big picture especially when focusing on application layer traffic.

 #include "stdafx.h"
#include "windows.h"
#include "stdio.h"
#include "stdlib.h"
#include "objbase.h"
#include "ntddndis.h"
#include "NMApi.h"

HANDLE g_NplParser = INVALID_HANDLE_VALUE;
HANDLE g_FrameParserConfig = INVALID_HANDLE_VALUE;

ULONG g_DescPropID = 0;    // Global Description Property ID.

// Callback for parser building messages
void __stdcall
MyParserBuild(PVOID Context, ULONG StatusCode, LPCWSTR lpDescription, ULONG ErrorType)
{
    wprintf(L"%s\n", lpDescription);
}

// Returns a frame parser with a filter and one data field.
// INVALID_HANDLE_VALUE indicates failure.
HANDLE
MyLoadNPL(void)
{
    HANDLE myFrameParser = INVALID_HANDLE_VALUE;
    ULONG ret;

    // Use NULL to load default NPL set.
    ret = NmLoadNplParser(NULL, NmAppendRegisteredNplSets, MyParserBuild, 0, &g_NplParser);

    if(ret == ERROR_SUCCESS){
        ret = NmCreateFrameParserConfiguration(g_NplParser, MyParserBuild, 0, &g_FrameParserConfig);

        if(ret == ERROR_SUCCESS)
        {
            // Order is important here, must turn on Conversations before Reasembly.
            ret = NmConfigConversation(g_FrameParserConfig, NmConversationOptionNone , TRUE);
            if(ret != ERROR_SUCCESS)
            {
                wprintf(L"Failed to config reassembly, error 0x%X\n", ret);
            }

            ret = NmConfigReassembly(g_FrameParserConfig, NmReassemblyOptionNone , TRUE);
            if(ret != ERROR_SUCCESS)
            {
                wprintf(L"Failed to config reassembly, error 0x%X\n", ret);
            }

            // Property so we can show the highest protocol description.
            ret = NmAddProperty(g_FrameParserConfig, L"property.Description", &g_DescPropID);
            if(ret != ERROR_SUCCESS)
            {
                wprintf(L"Failed to add field, error 0x%X\n", ret);
            }

            ret = NmCreateFrameParser(g_FrameParserConfig, &myFrameParser, NmParserOptimizeNone);

            if(ret != ERROR_SUCCESS)
            {
                wprintf(L"Failed to create frame parser, error 0x%X\n", ret);
                NmCloseHandle(g_FrameParserConfig);
                NmCloseHandle(g_NplParser);
                return INVALID_HANDLE_VALUE;
            }
        }
        else
        {
            wprintf(L"Unable to load parser config, error 0x%X\n", ret);
            NmCloseHandle(g_NplParser);
            return INVALID_HANDLE_VALUE;
        }

    }
    else
    {
        wprintf(L"Unable to load NPL\n");
        return INVALID_HANDLE_VALUE;
    }

    return(myFrameParser);
}

void
UnLoadNPL(void)
{
    NmCloseHandle(g_NplParser);
    NmCloseHandle(g_FrameParserConfig);
}

ULONG
GetFragType(HANDLE parsedFrame, NM_FRAGMENTATION_INFO *FragInfo)
{
    ULONG ret;

    FragInfo->Size = sizeof(FragInfo);
    ret = NmGetFrameFragmentInfo(parsedFrame, FragInfo);

    return ret;
}

PBYTE
GetDescription(HANDLE frameParser)
{
    ULONG ret;
    NM_PROPERTY_INFO PropInfo;

    // Find out the size of the description property so we can allocate a buffer.
    // MUST intialize the size and name pointer or NmGetPropertyInfo will fail.
    PropInfo.Size = sizeof(PropInfo);
    PropInfo.Name = NULL;
    ret = NmGetPropertyInfo(frameParser, g_DescPropID, &PropInfo);
    if(ret != ERROR_SUCCESS)
    {
        wprintf(L"Error calling NmGetPropertyInfo, %d\n", ret);
        return NULL;
    }

    ULONG retlen = 0;
    NmPropertyValueType propType;
    // Add size of WCHAR for null terminator
    PBYTE buf = (PBYTE)malloc(PropInfo.ValueSize + sizeof(WCHAR));
    ret = NmGetPropertyValueById(frameParser, g_DescPropID, PropInfo.ValueSize, buf, &retlen, &propType);
    if(ret != ERROR_SUCCESS)
    {
        wprintf(L"Error calling NmGetPropertyValueById, %d\n", ret);
        return NULL;
    }

    return buf;
}

// Recursive Parsing routine.  If an inserted frame is found, the recusive routine is called again.  This
// allows us to close our handles in the order there were created.
void
MyParseFrame(HANDLE frameParser, HANDLE rawFrame, ULONG curFrame, PULONG reassembleFrames, int reassembleCount)
{
    ULONG ret;
    HANDLE ParsedFrame = INVALID_HANDLE_VALUE;
    HANDLE InsRawFrame = INVALID_HANDLE_VALUE;

    // NmUseFrameNumber and valid unique frame numbers are neccessary for Reassembly to work properly.
    ret = NmParseFrame(frameParser, rawFrame, curFrame + *reassembleFrames, NmFieldDisplayStringRequired | NmUseFrameNumberParameter, &ParsedFrame, &InsRawFrame); 
    if(ret == ERROR_SUCCESS)
    {
        // Returns the highest level protocol description just to show which
        // frame we are working on.
        PBYTE buf = GetDescription(frameParser);

        // Get the fragment information which helps understand what is happening,
        // but not needed for reassembly to work.
        NM_FRAGMENTATION_INFO FragInfo;
        GetFragType(ParsedFrame, &FragInfo);

        wprintf(L"%5d-%d: %5d      %-5.5s-%d    %-.45s\n", curFrame+1, reassembleCount, curFrame+(*reassembleFrames)+1, FragInfo.FragmentedProtocolName, FragInfo.FragmentType, buf);

        free(buf);

        if(InsRawFrame != INVALID_HANDLE_VALUE)
        {
            (*reassembleFrames)++;
            MyParseFrame(frameParser, InsRawFrame, curFrame, reassembleFrames, reassembleCount+1);

            NmCloseHandle(InsRawFrame);
        }
    }

    NmCloseHandle(ParsedFrame);
    NmCloseHandle(InsRawFrame);
}

int __cdecl wmain(int argc, WCHAR* argv[])
{
    ULONG ret = ERROR_SUCCESS;
    // The first paramryrt should be a file.
    if(argc <= 1){
        wprintf(L"Expect a file name as the only command line parameter\n");
        return -1;
    }

    // Open the specified capture file.
    HANDLE myCaptureFile = INVALID_HANDLE_VALUE;
    if(ERROR_SUCCESS == NmOpenCaptureFile(argv[1], &myCaptureFile))
    {
        // Initialize the parser engine and return a frame parser.
        HANDLE myFrameParser = MyLoadNPL();
        if(myFrameParser != INVALID_HANDLE_VALUE)
        {
            ULONG myFrameCount = 0;
            ret = NmGetFrameCount(myCaptureFile, &myFrameCount); 
            if(ret == ERROR_SUCCESS)
            {
                ULONG totReassembledFrames = 0;
                HANDLE myRawFrame = INVALID_HANDLE_VALUE;

                wprintf(L"Frame#   Reassem#  FragType    Description\n");
                for(ULONG i = 0; i < myFrameCount; i++)
                {
                    HANDLE myParsedFrame = INVALID_HANDLE_VALUE;
                    ret = NmGetFrame(myCaptureFile, i, &myRawFrame); 
                    if(ret == ERROR_SUCCESS)
                    {
                        MyParseFrame(myFrameParser, myRawFrame, i, &totReassembledFrames, 0);

                        NmCloseHandle(myRawFrame);
                    }
                    else
                    {
                        // Print an error, but continue to loop.
                        wprintf(L"Errors getting raw frame %d\n", i+1);
                    }
                }
            }

            NmCloseHandle(myFrameParser);
        }
        else
        {
            wprintf(L"Errors creating frame parser\n");
        }

        NmCloseHandle(myCaptureFile);
    }
    else
    {
        wprintf(L"Errors openning capture file\n");
    }

    // Release global handles.
    UnLoadNPL();

    return 0;
}