PwnAdventure3 – Building a Wireshark parser

Lesezeit: 6 Minuten

Wireshark is one of the best – if not the best – packet analyser available. It allows you to capture the traffic sent from/to your machine and parse its content in order to have a human readable representation of it. At the moment, there are hundreds of supported protocols and media. Considering that the protocol of Pwn Adventure 3 is custom and not widely used, there is no dissector (parser) installed by default in Wireshark for this protocol. Eric – maetrics – Gragsone has already published a custom dissector in Lua, which was helpful for the realisation of this blog series. However, the dissector is missing some information that we covered in the last blog. Instead of re-using and improving the parser, we will start from scratch so I can explain the process and logic to build a Wireshark dissector.

You can implement a new dissector in two ways: either you build and compile the dissector directly into the main program, or you create an external plugin. If we wanted to develop a dissector for a long term project, we would have built the parser directly into the main program, however, in our case, we will use the plugin approach. Since we are in the context of a reverse engineering task, we want something easy to build and rapidly deployed. Wireshark supports Lua, an easy scripting language, which is ideal for this short tutorial.

Wireshark has an embedded Lua interpreter. Lua is a powerful light-weight programming language designed for extending applications.

In this post, I will consider that you already have some knowledge in Lua and move on straight to the dissector code. Our code will be based on the API reference and this presentation from the SHARKFEST 2009.

We first need to create a new protocol with the function Proto():

PWN3 = Proto ("pwn3", "Pwn Adventure 3 - Game server protocol")

We then need to define the different elements of the packets as listed in the previous post with PROTO.fields. We than create the element with ProtoField.newProtoField.uint8ProtoField.uint16 and ProtoField.uint32 depending on the size and the way we want to represent the value.

local f = PWN3.fields

local opcodes = {
    [0x0200] = "Authentication",
    [0x1600] = "Spawn position [1]",
    [0x1700] = "Spawn position [2]",
    [0x2300] = "Spawn position [3]",
    [0x232a] = "Send message",
    [0x233e] = "Send answer",
    [0x2366] = "Finish dialog",
    [0x2373] = "Send dialog",
    [0x2462] = "Purchase item",
    [0x2a69] = "Fire",
    [0x2b2b] = "Update health",
    [0x3003] = "Spawn position [4]",
    [0x3031] = "Activate logic gate",
    [0x3206] = "Spawn position [5]",
    [0x4103] = "Spawn position [6]",
    [0x5e64] = "Remove quest",
    [0x6368] = "Change location",
    [0x6370] = "New inventory item",
    [0x6565] = "Pick up item",
    [0x6576] = "Event",
    [0x6674] = "Fast travel",
    [0x6a70] = "Jump",
    [0x6d61] = "Update mana",
    [0x6d6b] = "New element",
    [0x6d76] = "Update location",
    [0x6e71] = "New quest",
    [0x7073] = "Enemy position",
    [0x7075] = "New achievement",
    [0x7076] = "Change PvP state",
    [0x713d] = "Select quest",
    [0x713e] = "Quest done",
    [0x726d] = "Remove inventory item",
    [0x726e] = "Run",
    [0x7273] = "Respawn",
    [0x7274] = "Teleport",
    [0x7374] = "Change player state",
    [0x7472] = "Change attack state",
    [0x7878] = "Remove element"

local status = {
    [0x00] = "Disable",
    [0x01] = "Enable"

local speed = {
    [0x00] = "Walk",
    [0x01] = "Run"

local moves = {
    [0x00] = "Neutral",
    [0x7f] = "Forward",
    [0x81] = "Backward"

local strafes = {
  [0x00] = "Neutral",
  [0x7f] = "Right",
  [0x81] = "Left"

f.opcode = ProtoField.uint16 ("pwn3.opcode", "Action", base.HEX, opcodes)

f.posx = ("X coordinate", "pwn3.posx", ftypes.FLOAT)
f.posy = ("Y coordinate", "pwn3.posy", ftypes.FLOAT)
f.posz = ("Z coordinate", "pwn3.posz", ftypes.FLOAT)

f.dirr = ProtoField.uint16 ("pwn3.dirr", "Direction roll", base.DEC)
f.diry = ProtoField.uint16 ("pwn3.diry", "Direction yaw", base.DEC)
f.dirp = ProtoField.uint16 ("pwn3.dirp", "Direction pitch", base.DEC)

f.vx = ProtoField.uint32 ("pwn3.vx", "Vector X", base.DEC)
f.vy = ProtoField.uint32 ("pwn3.vy", "Vector Y", base.DEC)
f.vz = ProtoField.uint32 ("pwn3.vz", "Vector Z", base.DEC) = ProtoField.uint8 ("", "Move", base.HEX, moves)
f.stf = ProtoField.uint8 ("pwn3.stf", "Strafe", base.HEX, strafes)

f.mana = ProtoField.uint32 ("pwn3.mana", "Mana", base.DEC) = ProtoField.uint32 ("", "Health", base.DEC)

f.gate = ProtoField.uint32 ("pwn3.gate", "Gate", base.DEC)

f.elid = ProtoField.uint16 ("pwn3.elid", "Element ID", base.DEC)
f.tid = ProtoField.uint16 ("pwn3.tid", "Target ID", base.DEC)

f.qt = ProtoField.uint16 ("pwn3.qt", "Quantity", base.DEC)

f.str = ProtoField.string ("pwn3.str", "String")

f.pvp = ProtoField.uint16 ("pwn3.pvp", "PvP status", base.HEX, status) = ProtoField.uint16 ("", "Speed", base.HEX, speed)

f.unknown = ProtoField.uint8 ("pwn3.unknown", "Unknown", base.HEX)

Now we can start dissecting the packet and adding elements in Wireshark tree structure. Everything happen in the PROTO.dissector() function:

function PWN3.dissector (buffer, pinfo, tree)

    -- Dissection code
    -- buffer is the data in the TCP packet
    -- tree is the root element in the visual tree structure


When a packet is selected in the Wireshark visual tree structure, the binary equivalent is highlighted (in blue).

For this, we simply need to do:

-- Add node to the root (right below the TCP element)
local subtree = tree:add (PROTO, buffer())

-- Add a node to subtree (created above)
local branch = subtree:add (f.FMT, buffer(OFFSET, LENGTH))

Where PROTO is the protocol defined earlier (i.e. PWN3), BUFFER is the buffer argument given in PROTO.dissector(),  f.FMT is the format defined previously, OFFSET and LENGTH is used to select a part of the buffer.

Since each packet has a different format and a different size, we first need to identify the packet (e.g. „Fire“, „Update location“, etc) so we can calculate the total size and thus highlight the right part of the binary value when selected.

In the following example, I will define the dissector for the „Update location“ and „Fire“ packets. I will try to comment the code as much as possible to be self-explanatory.

function PWN3.dissector (buffer, pinfo, tree)
    -- Create the "Pwn Adventure 3" node
    local subtree = tree:add (PWN3, buffer())
    -- Pointer to read through the buffer
    local offset = 0
    -- Read until we reach the end of the buffer
    while (offset < buffer:len()-1) do
        -- Reading two bytes
        -- opcode is the packet identifier number
        local opcode = buffer(offset, 2):uint()
        offset = offset + 2
        -- Update mana
        if (opcode == 0x6d61) then
            -- Adding a node "Update mana" in subtree
            -- Using the name related to the opcode
            local branch = subtree:add (buffer(offset-2, 6), opcodes[opcode])
            -- Adding a node in the "Update mane" branch
            -- Adding the opcode
            branch:add (f.opcode, buffer(offset-2, 2))
            -- Appending a text to node "Update mana"
            branch:append_text (", Mana: " .. buffer(offset, 4):le_uint())
            -- Adding a node in the "Update mana" branch
            -- Node mana level
            branch:add_le (f.mana, buffer(offset, 4))
            offset = offset + 4
        -- Fire
        elseif (opcode == 0x2a69) then
            -- Getting the size of the weapon name
            -- This is to calculate the total length of the packet
            local length = buffer(offset, 2):le_uint()
            -- Adding a node "Fire" in subtree
            -- Using the name related to the opcode
            -- The size of the packet is:
            -- 2 bytes for the opcode
            -- 2 bytes for the length of the weapons name
            -- n bytes for the weapons name (stored in var length)
            -- 12 bytes for the direction of the projectile
            -- Total = 16 + length
            local branch = subtree:add (buffer(offset-2, 16+length), opcodes[opcode])
            -- Adding a node in the "Update mane" branch
            -- Adding the opcode
            branch:add (f.opcode, buffer(offset-2, 2))
            -- Skip the length byte
            offset = offset + 2

            -- Appending a text to node "Fire"
            branch:append_text (", Weapon: " .. buffer(offset, length):string())
            -- Adding a node in the "Fire" branch
            -- Node weapons name
            branch:add (f.str, buffer(offset, length))
            offset = offset + length
            -- Adding a node in the "Fire" branch
            -- Node direction of the projectile
            addVectors (buffer, offset, branch)
            offset = offset + 12

As you may have noticed, since the location, the direction and the vectors are called multiple times, I decided to create functions instead:

function addLocation (location, offset, tree)

    local branch

    branch = tree:add (location(offset, 12), "Location")

    branch:add_le (f.posx, location(offset, 4))
    branch:add_le (f.posy, location(offset + 4, 4))
    branch:add_le (f.posz, location(offset + 8, 4))


function addDirection (direction, offset, tree)

    local branch

    branch = tree:add (direction(offset, 6), "Direction")

    branch:add_le (f.dirr, direction(offset, 2))
    branch:add_le (f.diry, direction(offset + 2, 2))
    branch:add_le (f.dirp, direction(offset + 4, 2))


function addVectors (vectors, offset, tree)

    local branch

    branch = tree:add (vectors(offset, 12), "Vectors")

    branch:add_le (f.vx, vectors(offset, 4))
    branch:add_le (f.vy, vectors(offset + 4, 4))
    branch:add_le (f.vz, vectors(offset + 8, 4))


Now we need to register the protocol to the right ports thanks to the DissectorTable. The game server allocates a new instance on a different ports if one is too crowded. Here, we expect the server to use only 4 instances.

tcp_table = DissectorTable.get ("tcp.port")
tcp_table:add (3000, PWN3)
tcp_table:add (3001, PWN3)
tcp_table:add (3002, PWN3)
tcp_table:add (3003, PWN3)

Now the dissector should be saved in the plugin folder of Wireshark. The path can be found in the „About Wireshark“ window:

  • macOS: „Wireshark“ > „About Wireshark“ > „Folders“
  • Linux: „Help“ > „About Wireshark“ > „Folders“
  • Windows: „Help“ > „About Wireshark“ > „Folders“

Once saved in the appropriate folder, the new dissector should be loaded automatically when Wireshark is launched, however you still can reload the Lua dissector in „Analyse“ > „Reload Lua Plugins“. If you still can’t find your packet properly parsed, make sure that the PWN3 protocol is enabled in „Analyse“ > „Enabled Protocol…“.

In the given example, we only parse the „Update mana“ and „Fire“ packets. You will find the complete dissector here in our GitHub page. You should now be able to parse the network traffic generated between the client and the Game server(s).

Blog series: IntroductionReverse Engineering Network ProtocolPwn Adventure 3 Network ProtocolBuilding a Wireshark ParserAsynchronous Proxy in PythonIntercepting Packets – Reverse Engineering BinaryPatching BinaryHooking shared library

Von |2017-06-20T12:00:36+00:0020. Juni, 2017 um 12:00 Uhr|KEYIDENTITY|Noch keine Kommentare

Über den Autor:

Manuela Kohlhas