wireshark/epan/dissectors/packet-rdp_egfx.c
David Fort 09f762ba5e rdp: add dissector for the egfx channel
This patch adds basic dissection for the egfx channel. It also fixes fragmentation
in the dynamic channel, and also introduces some of the decompressors involved in RDP
traffic.
2021-10-02 11:15:32 +02:00

553 lines
17 KiB
C

/* Packet-rdp_egfx.c
* Routines for the EGFX RDP channel
* Copyright 2021, David Fort <contact@hardening-consulting.com>
*
* Wireshark - Network traffic analyzer
* By Gerald Combs <gerald@wireshark.org>
* Copyright 1998 Gerald Combs
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
/*
* See: "[MS-RDPEGFX] "
*/
#include "config.h"
#include <epan/packet.h>
#include <epan/prefs.h>
#include <epan/conversation.h>
#include <epan/expert.h>
#include <epan/tvbuff_rdp.h>
#include "packet-rdp.h"
#include "packet-rdpudp.h"
void proto_register_rdp_egfx(void);
void proto_reg_handoff_rdp_egfx(void);
static int proto_rdp_egfx = -1;
static int hf_egfx_cmdId = -1;
static int hf_egfx_flags = -1;
static int hf_egfx_pduLength = -1;
static int hf_egfx_caps_capsSetCount = -1;
static int hf_egfx_cap_version = -1;
static int hf_egfx_cap_length = -1;
static int hf_egfx_reset_width = -1;
static int hf_egfx_reset_height = -1;
static int hf_egfx_reset_monitorCount = -1;
static int hf_egfx_reset_monitorDefLeft = -1;
static int hf_egfx_reset_monitorDefTop = -1;
static int hf_egfx_reset_monitorDefRight = -1;
static int hf_egfx_reset_monitorDefBottom = -1;
static int hf_egfx_reset_monitorDefFlags = -1;
static int hf_egfx_ack_queue_depth = -1;
static int hf_egfx_ack_frame_id = -1;
static int hf_egfx_ack_total_decoded = -1;
static int hf_egfx_ackqoe_frame_id = -1;
static int hf_egfx_ackqoe_timestamp = -1;
static int hf_egfx_ackqoe_timediffse = -1;
static int hf_egfx_ackqoe_timediffedr = -1;
static int hf_egfx_start_timestamp = -1;
static int hf_egfx_start_frameid = -1;
static int hf_egfx_end_frameid = -1;
static int ett_rdp_egfx = -1;
static int ett_egfx_caps = -1;
static int ett_egfx_capsconfirm = -1;
static int ett_egfx_cap = -1;
static int ett_egfx_ack = -1;
static int ett_egfx_ackqoe = -1;
static int ett_egfx_reset = -1;
static int ett_egfx_monitors = -1;
static int ett_egfx_monitordef = -1;
static expert_field ei_egfx_pdulen_invalid = EI_INIT;
static expert_field ei_egfx_invalid_compression = EI_INIT;
#define PNAME "RDP Graphic pipeline channel Protocol"
#define PSNAME "EGFX"
#define PFNAME "rdp_egfx"
enum {
RDPGFX_CMDID_WIRETOSURFACE_1 = 0x0001,
RDPGFX_CMDID_WIRETOSURFACE_2 = 0x0002,
RDPGFX_CMDID_DELETEENCODINGCONTEXT = 0x0003,
RDPGFX_CMDID_SOLIDFILL = 0x0004,
RDPGFX_CMDID_SURFACETOSURFACE = 0x0005,
RDPGFX_CMDID_SURFACETOCACHE = 0x0006,
RDPGFX_CMDID_CACHETOSURFACE = 0x0007,
RDPGFX_CMDID_EVICTCACHEENTRY = 0x0008,
RDPGFX_CMDID_CREATESURFACE = 0x0009,
RDPGFX_CMDID_DELETESURFACE = 0x000a,
RDPGFX_CMDID_STARTFRAME = 0x000b,
RDPGFX_CMDID_ENDFRAME = 0x000c,
RDPGFX_CMDID_FRAMEACKNOWLEDGE = 0x000d,
RDPGFX_CMDID_RESETGRAPHICS = 0x000e,
RDPGFX_CMDID_MAPSURFACETOOUTPUT = 0x000f,
RDPGFX_CMDID_CACHEIMPORTOFFER = 0x0010,
RDPGFX_CMDID_CACHEIMPORTREPLY = 0x0011,
RDPGFX_CMDID_CAPSADVERTISE = 0x0012,
RDPGFX_CMDID_CAPSCONFIRM = 0x0013,
RDPGFX_CMDID_MAPSURFACETOWINDOW = 0x0015,
RDPGFX_CMDID_QOEFRAMEACKNOWLEDGE = 0x0016,
RDPGFX_CMDID_MAPSURFACETOSCALEDOUTPUT = 0x0017,
RDPGFX_CMDID_MAPSURFACETOSCALEDWINDOW = 0x0018,
};
enum {
RDPGFX_CAPVERSION_8 = 0x00080004,
RDPGFX_CAPVERSION_81 = 0x00080105,
RDPGFX_CAPVERSION_101 = 0x000A0100,
RDPGFX_CAPVERSION_102 = 0x000A0200,
RDPGFX_CAPVERSION_103 = 0x000A0301,
RDPGFX_CAPVERSION_104 = 0x000A0400,
RDPGFX_CAPVERSION_105 = 0x000A0502,
RDPGFX_CAPVERSION_106 = 0x000A0600
};
static const value_string rdp_egfx_cmd_vals[] = {
{ RDPGFX_CMDID_WIRETOSURFACE_1, "Wire to surface 1" },
{ RDPGFX_CMDID_WIRETOSURFACE_2, "Wire to surface 2" },
{ RDPGFX_CMDID_DELETEENCODINGCONTEXT, "delete encoding context" },
{ RDPGFX_CMDID_SOLIDFILL, "Solid fill" },
{ RDPGFX_CMDID_SURFACETOSURFACE, "Surface to surface" },
{ RDPGFX_CMDID_SURFACETOCACHE, "Surface to cache" },
{ RDPGFX_CMDID_CACHETOSURFACE, "Cache to surface" },
{ RDPGFX_CMDID_EVICTCACHEENTRY, "Evict cache entry" },
{ RDPGFX_CMDID_CREATESURFACE, "Create surface" },
{ RDPGFX_CMDID_DELETESURFACE, "Delete surface" },
{ RDPGFX_CMDID_STARTFRAME, "Start frame" },
{ RDPGFX_CMDID_ENDFRAME, "End frame" },
{ RDPGFX_CMDID_FRAMEACKNOWLEDGE, "Frame acknowlegde" },
{ RDPGFX_CMDID_RESETGRAPHICS, "Reset graphics" },
{ RDPGFX_CMDID_MAPSURFACETOOUTPUT, "Map Surface to output" },
{ RDPGFX_CMDID_CACHEIMPORTOFFER, "Cache import offer" },
{ RDPGFX_CMDID_CACHEIMPORTREPLY, "Cache import reply" },
{ RDPGFX_CMDID_CAPSADVERTISE, "Caps advertise" },
{ RDPGFX_CMDID_CAPSCONFIRM, "Caps confirm" },
{ RDPGFX_CMDID_MAPSURFACETOWINDOW, "Map surface to window" },
{ RDPGFX_CMDID_QOEFRAMEACKNOWLEDGE, "Qoe frame acknowlegde" },
{ RDPGFX_CMDID_MAPSURFACETOSCALEDOUTPUT, "Map surface to scaled output" },
{ RDPGFX_CMDID_MAPSURFACETOSCALEDWINDOW, "Map surface to scaled window" },
{ 0x0, NULL },
};
static const value_string rdp_egfx_caps_version_vals[] = {
{ RDPGFX_CAPVERSION_8, "8" },
{ RDPGFX_CAPVERSION_81, "8.1" } ,
{ RDPGFX_CAPVERSION_101, "10.1" },
{ RDPGFX_CAPVERSION_102, "10.2" },
{ RDPGFX_CAPVERSION_103, "10.3" },
{ RDPGFX_CAPVERSION_104, "10.4" },
{ RDPGFX_CAPVERSION_105, "10.5" },
{ RDPGFX_CAPVERSION_106, "10.6" },
{ 0x0, NULL },
};
static const value_string rdp_egfx_monitor_flags_vals[] = {
{ 0x00000000, "is secondary" },
{ 0x00000001, "is primary" },
{ 0x0, NULL },
};
typedef struct {
zgfx_context_t *zgfx;
} egfx_conv_info_t;
static egfx_conv_info_t *
egfx_get_conversation_data(packet_info *pinfo)
{
conversation_t *conversation, *conversation_tcp;
egfx_conv_info_t *info;
conversation = find_or_create_conversation(pinfo);
info = (egfx_conv_info_t *)conversation_get_proto_data(conversation, proto_rdp_egfx);
if (!info) {
conversation_tcp = rdp_find_tcp_conversation_from_udp(conversation);
if (conversation_tcp)
info = (egfx_conv_info_t *)conversation_get_proto_data(conversation_tcp, proto_rdp_egfx);
}
if (info == NULL) {
info = wmem_new0(wmem_file_scope(), egfx_conv_info_t);
info->zgfx = zgfx_context_new(wmem_file_scope());
conversation_add_proto_data(conversation, proto_rdp_egfx, info);
}
return info;
}
static int
dissect_rdp_egfx_payload(tvbuff_t *tvb, packet_info *pinfo, proto_tree *parent_tree, void *data _U_)
{
proto_item *item;
proto_tree *tree;
proto_tree *subtree;
gint offset = 0;
guint16 cmdId = 0;
guint32 pduLength;
guint32 i;
parent_tree = proto_tree_get_root(parent_tree);
col_set_str(pinfo->cinfo, COL_PROTOCOL, "EGFX");
col_clear(pinfo->cinfo, COL_INFO);
while (tvb_captured_length_remaining(tvb, offset) > 8) {
pduLength = tvb_get_guint32(tvb, offset + 4, ENC_LITTLE_ENDIAN);
item = proto_tree_add_item(parent_tree, proto_rdp_egfx, tvb, offset, pduLength, ENC_NA);
tree = proto_item_add_subtree(item, ett_rdp_egfx);
cmdId = tvb_get_guint16(tvb, offset, ENC_LITTLE_ENDIAN);
proto_tree_add_item(tree, hf_egfx_cmdId, tvb, offset, 2, ENC_LITTLE_ENDIAN);
offset += 2;
proto_tree_add_item(tree, hf_egfx_flags, tvb, offset, 2, ENC_LITTLE_ENDIAN);
offset += 2;
proto_tree_add_item(tree, hf_egfx_pduLength, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
if (pduLength < 8) {
expert_add_info_format(pinfo, item, &ei_egfx_pdulen_invalid, "pduLength is %u, not < 8", pduLength);
return offset;
}
switch (cmdId) {
case RDPGFX_CMDID_CAPSADVERTISE: {
guint16 capsSetCount = tvb_get_guint16(tvb, offset, ENC_LITTLE_ENDIAN);
col_append_sep_str(pinfo->cinfo, COL_INFO, ",", "Caps advertise");
proto_tree_add_item(tree, hf_egfx_caps_capsSetCount, tvb, offset, 2, ENC_LITTLE_ENDIAN);
subtree = proto_tree_add_subtree(tree, tvb, offset, pduLength-8, ett_egfx_caps, NULL, "Caps");
offset += 2;
for (i = 0; i < capsSetCount; i++) {
guint32 capsDataLength;
proto_tree_add_item(subtree, hf_egfx_cap_version, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
proto_tree_add_item(subtree, hf_egfx_cap_length, tvb, offset, 4, ENC_LITTLE_ENDIAN);
capsDataLength = tvb_get_guint32(tvb, offset, ENC_LITTLE_ENDIAN);
offset += 4;
offset += capsDataLength;
}
break;
}
case RDPGFX_CMDID_CAPSCONFIRM: {
guint32 capsDataLength;
col_append_sep_str(pinfo->cinfo, COL_INFO, ",", "Caps confirm");
subtree = proto_tree_add_subtree(tree, tvb, offset, pduLength-8, ett_egfx_capsconfirm, NULL, "Caps confirm");
proto_tree_add_item(subtree, hf_egfx_cap_version, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
proto_tree_add_item_ret_uint(subtree, hf_egfx_cap_length, tvb, offset, 4, ENC_LITTLE_ENDIAN, &capsDataLength);
offset += 4 + capsDataLength;
break;
}
case RDPGFX_CMDID_RESETGRAPHICS: {
guint32 nmonitor;
proto_tree *monitors_tree;
col_append_sep_str(pinfo->cinfo, COL_INFO, ",", "Reset graphics");
subtree = proto_tree_add_subtree(tree, tvb, offset, pduLength-4, ett_egfx_reset, NULL, "Reset graphics");
proto_tree_add_item(subtree, hf_egfx_reset_width, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
proto_tree_add_item(subtree, hf_egfx_reset_height, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
proto_tree_add_item_ret_uint(subtree, hf_egfx_reset_monitorCount, tvb, offset, 4, ENC_LITTLE_ENDIAN, &nmonitor);
offset += 4;
monitors_tree = proto_tree_add_subtree(subtree, tvb, offset, nmonitor * 20, ett_egfx_monitors, NULL, "Monitors");
for (i = 0; i < nmonitor; i++) {
proto_item *monitor_tree;
guint32 left, top, right, bottom;
left = tvb_get_guint32(tvb, offset, ENC_LITTLE_ENDIAN);
top = tvb_get_guint32(tvb, offset+4, ENC_LITTLE_ENDIAN);
right = tvb_get_guint32(tvb, offset+8, ENC_LITTLE_ENDIAN);
bottom = tvb_get_guint32(tvb, offset+12, ENC_LITTLE_ENDIAN);
monitor_tree = proto_tree_add_subtree_format(monitors_tree, tvb, offset, 20, ett_egfx_monitordef, NULL,
"(%d,%d) - (%d,%d)", left, top, right, bottom);
proto_tree_add_item(monitor_tree, hf_egfx_reset_monitorDefLeft, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
proto_tree_add_item(monitor_tree, hf_egfx_reset_monitorDefTop, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
proto_tree_add_item(monitor_tree, hf_egfx_reset_monitorDefRight, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
proto_tree_add_item(monitor_tree, hf_egfx_reset_monitorDefBottom, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
proto_tree_add_item(monitor_tree, hf_egfx_reset_monitorDefFlags, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
}
offset += (pduLength - 8);
break;
}
case RDPGFX_CMDID_STARTFRAME: {
col_append_sep_str(pinfo->cinfo, COL_INFO, ",", "Start frame");
proto_tree_add_item(tree, hf_egfx_start_timestamp, tvb, offset, 4, ENC_LITTLE_ENDIAN);
// TODO: dissect timestamp
offset += 4;
proto_tree_add_item(tree, hf_egfx_start_frameid, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
break;
}
case RDPGFX_CMDID_ENDFRAME:
col_append_sep_str(pinfo->cinfo, COL_INFO, ",", "End frame");
proto_tree_add_item(tree, hf_egfx_end_frameid, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
break;
case RDPGFX_CMDID_FRAMEACKNOWLEDGE:
col_append_sep_str(pinfo->cinfo, COL_INFO, ",", "Frame acknowledge");
subtree = proto_tree_add_subtree(tree, tvb, offset, -1, ett_egfx_ack, NULL, "Frame acknowledge");
proto_tree_add_item(subtree, hf_egfx_ack_queue_depth, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
proto_tree_add_item(subtree, hf_egfx_ack_frame_id, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
proto_tree_add_item(subtree, hf_egfx_ack_total_decoded, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
break;
case RDPGFX_CMDID_QOEFRAMEACKNOWLEDGE:
col_append_sep_str(pinfo->cinfo, COL_INFO, ",", "Frame acknowledge QoE");
subtree = proto_tree_add_subtree(tree, tvb, offset, -1, ett_egfx_ackqoe, NULL, "Frame acknowledge QoE");
proto_tree_add_item(subtree, hf_egfx_ackqoe_frame_id, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
proto_tree_add_item(subtree, hf_egfx_ackqoe_timestamp, tvb, offset, 4, ENC_LITTLE_ENDIAN);
offset += 4;
proto_tree_add_item(subtree, hf_egfx_ackqoe_timediffse, tvb, offset, 2, ENC_LITTLE_ENDIAN);
offset += 2;
proto_tree_add_item(subtree, hf_egfx_ackqoe_timediffedr, tvb, offset, 2, ENC_LITTLE_ENDIAN);
offset += 2;
break;
default:
offset += (pduLength - 8);
break;
}
}
return offset;
}
static int
dissect_rdp_egfx(tvbuff_t *tvb, packet_info *pinfo, proto_tree *parent_tree, void *data)
{
tvbuff_t *work_tvb = tvb;
col_set_str(pinfo->cinfo, COL_PROTOCOL, "EGFX");
col_clear(pinfo->cinfo, COL_INFO);
parent_tree = proto_tree_get_root(parent_tree);
if (!rdp_isServerAddressTarget(pinfo)) {
egfx_conv_info_t *infos = egfx_get_conversation_data(pinfo);
work_tvb = rdp8_decompress(infos->zgfx, wmem_packet_scope(), tvb, 0);
if (!work_tvb && parent_tree) {
expert_add_info_format(pinfo, parent_tree->last_child, &ei_egfx_invalid_compression, "invalid compression");
return 0;
}
add_new_data_source(pinfo, work_tvb, "Uncompressed GFX");
}
dissect_rdp_egfx_payload(work_tvb, pinfo, parent_tree, data);
return tvb_reported_length(tvb);
}
void proto_register_rdp_egfx(void) {
static hf_register_info hf[] = {
{ &hf_egfx_cmdId,
{ "CmdId", "rdp_egfx.cmdid",
FT_UINT16, BASE_HEX, VALS(rdp_egfx_cmd_vals), 0x0,
NULL, HFILL }
},
{ &hf_egfx_flags,
{ "flags", "rdp_egfx.flags",
FT_UINT16, BASE_HEX, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_pduLength,
{ "pduLength", "rdp_egfx.pdulength",
FT_UINT32, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_caps_capsSetCount,
{ "capsSetCount", "rdp_egfx.caps.setcount",
FT_UINT16, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_cap_version,
{ "Version", "rdp_egfx.cap.version",
FT_UINT32, BASE_HEX, VALS(rdp_egfx_caps_version_vals), 0x0,
NULL, HFILL }
},
{ &hf_egfx_cap_length,
{ "capsDataLength", "rdp_egfx.cap.length",
FT_UINT32, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_ack_queue_depth,
{ "queueDepth", "rdp_egfx.ack.queuedepth",
FT_UINT32, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_ack_frame_id,
{ "frameId", "rdp_egfx.ack.frameid",
FT_UINT32, BASE_HEX, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_ack_total_decoded,
{ "Total frames decoded", "rdp_egfx.ack.totalframesdecoded",
FT_UINT32, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_ackqoe_frame_id,
{ "frameId", "rdp_egfx.ackqoe.frameid",
FT_UINT32, BASE_HEX, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_ackqoe_timestamp,
{ "Timestamp", "rdp_egfx.ackqoe.timestamp",
FT_UINT32, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_ackqoe_timediffse,
{ "TimeDiffSE", "rdp_egfx.ackqoe.timediffse",
FT_UINT16, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_ackqoe_timediffedr,
{ "TimeDiffEDR", "rdp_egfx.ackqoe.timediffedr",
FT_UINT16, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_reset_width,
{ "Width", "rdp_egfx.reset.width",
FT_UINT32, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_reset_height,
{ "Height", "rdp_egfx.reset.height",
FT_UINT32, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_reset_monitorCount,
{ "Monitor count", "rdp_egfx.reset.monitorcount",
FT_UINT32, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_reset_monitorDefLeft,
{ "Left", "rdp_egfx.monitor.left",
FT_UINT32, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_reset_monitorDefTop,
{ "Top", "rdp_egfx.monitor.top",
FT_UINT32, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_reset_monitorDefRight,
{ "Right", "rdp_egfx.monitor.right",
FT_UINT32, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_reset_monitorDefBottom,
{ "Bottom", "rdp_egfx.monitor.bottom",
FT_UINT32, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_reset_monitorDefFlags,
{ "Flags", "rdp_egfx.monitor.flags",
FT_UINT32, BASE_DEC, VALS(rdp_egfx_monitor_flags_vals), 0x0,
NULL, HFILL }
},
{ &hf_egfx_start_timestamp,
{ "Timestamp", "rdp_egfx.startframe.timestamp",
FT_UINT32, BASE_DEC, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_start_frameid,
{ "Frame id", "rdp_egfx.startframe.frameid",
FT_UINT32, BASE_HEX, NULL, 0x0,
NULL, HFILL }
},
{ &hf_egfx_end_frameid,
{ "Frame id", "rdp_egfx.endframe.frameid",
FT_UINT32, BASE_HEX, NULL, 0x0,
NULL, HFILL }
},
};
static gint *ett[] = {
&ett_rdp_egfx,
&ett_egfx_caps,
&ett_egfx_cap,
&ett_egfx_ack,
&ett_egfx_ackqoe,
&ett_egfx_reset,
&ett_egfx_capsconfirm,
&ett_egfx_monitors,
&ett_egfx_monitordef,
};
static ei_register_info ei[] = {
{ &ei_egfx_pdulen_invalid, { "rdp_egfx.pdulength.invalid", PI_PROTOCOL, PI_ERROR, "Invalid length", EXPFILL }},
{ &ei_egfx_invalid_compression, { "rdp_egfx.compression.invalid", PI_PROTOCOL, PI_ERROR, "Invalid compression", EXPFILL }},
};
expert_module_t* expert_egfx;
proto_rdp_egfx = proto_register_protocol(PNAME, PSNAME, PFNAME);
/* Register fields and subtrees */
proto_register_field_array(proto_rdp_egfx, hf, array_length(hf));
proto_register_subtree_array(ett, array_length(ett));
expert_egfx = expert_register_protocol(proto_rdp_egfx);
expert_register_field_array(expert_egfx, ei, array_length(ei));
register_dissector("rdp_egfx", dissect_rdp_egfx, proto_rdp_egfx);
}
void proto_reg_handoff_rdp_egfx(void) {
}