├── .gitignore ├── README.md ├── img ├── screen_1.png └── screen_2.png └── src ├── main.lua ├── protocols ├── mplex │ └── mplex.lua ├── multistream │ ├── multistream.lua │ └── multistream_state.lua └── secio │ ├── openssl_ffi.lua │ ├── proto.lua │ ├── secio.lua │ ├── secio_misc.lua │ └── secio_state.lua └── utils ├── config.lua ├── length-prefixed.lua ├── net_addresses.lua ├── table_copy.lua └── uvarint.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .tags 2 | .tags_sorted_by_file 3 | tags 4 | cscope.* 5 | 6 | 7 | # MAC Stuff # 8 | ################# 9 | *.fuse_hidden* 10 | .DS_Store 11 | ._.DS_Store 12 | 13 | 14 | # Editor-specific for ST/VI/Emacs # 15 | ################################### 16 | *.sublime-* 17 | *.sw* 18 | *~ 19 | BROWSE 20 | 21 | # IDEA 22 | #################################### 23 | .idea/* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Libp2p dissector 2 | 3 | A Wireshark Lua plugin to dissect several libp2p protocols with support of SECIO decryption. This plugin indended to run with go-libp2p-secio [fork](https://github.com/michaelvoronov/go-libp2p-secio) since it supports dumping secret symmetric keys. 4 | 5 | ## Usage: 6 | Copy the whole directory into your Wireshark `Personal Plugins` folder. To find out where it is located, open Wireshark and go to **Help->About Wireshark** and it will be listed in the **Folders** tab. You may need to create the folder the first time. 7 | 8 | Another prerequisite is to define environment variable `LIBP2P_SECIO_KEYLOG` that should be pointer to the file with secret keys. 9 | 10 | To run plugin you need to open Wireshark, sniff (or load from a dump) network traffic and then activate plugin via **Help->About Wireshark**. 11 | 12 | ![*SECIO dissecting: example 1](https://raw.githubusercontent.com/michaelvoronov/secio-dissector/master/img/screen_1.png) 13 | 14 | ![*SECIO dissecting: example 2](https://raw.githubusercontent.com/michaelvoronov/secio-dissector/master/img/screen_2.png) 15 | 16 | ## Prerequisites 17 | 18 | You need some lua packets installed: 19 | - ffi (`luarocks install --server=http://luarocks.org/dev luaffi`) 20 | - pb (`luarocks install lua-protobuf`) 21 | - protoc (`luarocks install protoc`) 22 | - lua (`luarocks install csv`) 23 | - base64 (`luarocks install lbase64`) 24 | 25 | Please be sure, that Wireshark has access to these plugins on your setup. 26 | 27 | ## Supported protocols 28 | 29 | - [X] multistream 1.0.0 30 | - [X] secio 1.0.0 31 | - [X] mplex 1.0.0 32 | - [ ] yamux 33 | - [ ] spdy 34 | - [ ] ipfs 35 | 36 | ## High-level dissecting algorithm overview 37 | 38 | 1. At first, multistream dissector is registred as the heuristic dissector 39 | 2. This dissecctor looks for the "/multistream/1.0.0" string in traffic, then parses multistream handshaked packets and, finally, calls secio dissector. 40 | 3. In its turn, secio dissector waits for Propose and Excahnge packets. And after receiving the last Exchange packet will try to open config file and try to find the last record for corresponding in/out ip:port. It is expected that after the last Exchange packet keys are already dumped. 41 | 4. And, finally, dissect mplex. -------------------------------------------------------------------------------- /img/screen_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikevoronov/libp2p-dissector/f82ebab73f44b02193f532688c9af5dbbad7e71a/img/screen_1.png -------------------------------------------------------------------------------- /img/screen_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikevoronov/libp2p-dissector/f82ebab73f44b02193f532688c9af5dbbad7e71a/img/screen_2.png -------------------------------------------------------------------------------- /src/main.lua: -------------------------------------------------------------------------------- 1 | local libp2p_dissector_version = "0.1.5" 2 | 3 | -- latest development release of Wireshark supports plugin version information 4 | if set_plugin_info then 5 | local libp2p_dissector_info = { 6 | version = libp2p_dissector_version, 7 | author = "Mike Voronov", 8 | email = "michail.vms@gmail.com", 9 | details = "This is a plugin for Wireshark to dissect libp2p messages.", 10 | repository = "https://github.com/michaelvoronov/libp2p-dissector", 11 | help = [[ 12 | HOW TO RUN THIS SCRIPT: 13 | Either copy the entire folder into your "Personal Plugins" directory or load it from the command line. 14 | ]] 15 | } 16 | set_plugin_info(libp2p_dissector_info) 17 | end 18 | 19 | -- enable loading of our modules 20 | _G['libp2p_dissector'] = {} 21 | 22 | -- check that LIBP2P_SECIO_KEYLOG set 23 | local key_file_path = os.getenv("LIBP2P_SECIO_KEYLOG") 24 | assert(secret == nil, "Environment variable LIBP2P_SECIO_KEYLOG must be set") 25 | print("libp2p dissector: use " .. key_file_path .. " as the key log file") 26 | 27 | -- help wireshark find other modules 28 | package.prepend_path("utils") 29 | package.prepend_path("protocols") 30 | package.prepend_path("protocols/multistream") 31 | package.prepend_path("protocols/secio") 32 | package.prepend_path("protocols/mplex") 33 | -------------------------------------------------------------------------------- /src/protocols/mplex/mplex.lua: -------------------------------------------------------------------------------- 1 | -- prevent wireshark loading this file as a plugin 2 | if not _G['libp2p_dissector'] then return end 3 | 4 | require ("uvarint") 5 | local bit = require ("bit32") 6 | 7 | local band = bit.band 8 | local rshift = bit.rshift 9 | 10 | local MPLEX_UVARINT_MAX_SIZE = 10 11 | 12 | mplex_proto = Proto ("mplex", "MPLEX protocol") 13 | 14 | function mplex_proto.dissector(buffer, pinfo, tree) 15 | pinfo.cols.protocol = "MPLEX" 16 | pinfo.cols.info = "MPLEX Body" 17 | 18 | local plain_text = pinfo.private["plain_text"] 19 | if (plain_text == nil) then 20 | print("mplex dissector: error while getting plain_text from the private field") 21 | return 22 | end 23 | 24 | local subtree = tree:add(mplex_proto, "MPLEX protocol") 25 | 26 | plain_text = ByteArray.new(plain_text) 27 | plain_text = ByteArray.tvb(plain_text, "plain text") 28 | 29 | local header, headerSize = extractUvarint(plain_text, MPLEX_UVARINT_MAX_SIZE) 30 | local headerTree = subtree:add(buffer(4, headerSize), string.format("MPLEX header: uvarint decoded 0x%X", header)) 31 | headerTree:add(buffer(4, headerSize), string.format("flags 0x%x", band(header, 0x7))) 32 | headerTree:add(buffer(4, headerSize), string.format("stream id 0x%x", rshift(header, 3))) 33 | 34 | local len, lenSize = extractUvarint(plain_text(headerSize, plain_text:len() - headerSize), MPLEX_UVARINT_MAX_SIZE) 35 | subtree:add(buffer(4 + headerSize, lenSize), string.format("MPLEX len: uvarint decoded 0x%X", len)) 36 | subtree:add(buffer(4 + headerSize + lenSize, plain_text:len() - lenSize - headerSize), "content") 37 | 38 | end 39 | -------------------------------------------------------------------------------- /src/protocols/multistream/multistream.lua: -------------------------------------------------------------------------------- 1 | -- prevent wireshark loading this file as a plugin 2 | if not _G['libp2p_dissector'] then return end 3 | 4 | local MSStates = require("multistream_state") 5 | local SecioStates = require ("secio_state") 6 | require("length-prefixed") 7 | require("net_addresses") 8 | 9 | multistream_proto = Proto ("multistream", "multistream 1.0.0 protocol") 10 | local fields = multistream_proto.fields 11 | 12 | -- Multistream protocol fields 13 | fields.multistream_protocol = ProtoField.string ("multistream.protocol", "Protocol", base.NONE, nil, 0, "Protocol being negotiated on") 14 | fields.multistream_raw_protocol = ProtoField.string ("multistream.raw_protocol", "Raw Protocol", base.NONE, nil, 0, "Protocol being negotiated on (only set on packets with raw data)") 15 | fields.multistream_version = ProtoField.string ("multistream.version", "Version", base.NONE, nil, 0, "Multistream version used") 16 | fields.multistream_dialer = ProtoField.bool ("multistream.dialer", "Dialer", base.NONE, nil, 0, "TRUE if the packet is sent from the dialer") 17 | fields.multistream_listener = ProtoField.bool ("multistream.listener", "Listener", base.NONE, nil, 0, "TRUE if the packet is sent from the listener") 18 | fields.multistream_handshake = ProtoField.bool ("multistream.handshake", "Handshake", base.NONE, nil, 0, "TRUE if the packet is part of the handshake process") 19 | fields.multistream_data = ProtoField.bytes ("multistream.data", "Data", base.NONE, nil, 0, "Raw bytes transferred") 20 | 21 | local function dissect_handshake(buffer, pinfo, state) 22 | local packet_len = buffer:len() 23 | local is_listener = false 24 | 25 | -- heuristic multistream detector should already set MSState.listener and MSState.dialer fields 26 | if (is_same_src_address(state.listener, pinfo)) then 27 | is_listener = true 28 | elseif (not is_same_src_address(state.dialer, pinfo)) then 29 | -- some error occured 30 | print("multistream dissector: ip:port are incorrect") 31 | return 32 | end 33 | 34 | if (is_listener) then 35 | if(state.listenerMSver == nil) then 36 | -- packet with protocol version 37 | if (packet_len < 1) then 38 | -- TODO: reassemble 39 | return 40 | end 41 | local protocol_name, _ = extract_lp_hex_string(buffer) 42 | if(protocol_name == nil) then 43 | -- TODO: reassemble 44 | return 45 | end 46 | 47 | state.listenerMSver = protocol_name:sub(1, -2) 48 | state.helloPacketId = pinfo.number 49 | return 50 | end 51 | -- ack/nack packets 52 | if (packet_len < 1) then 53 | -- TODO: reassemble 54 | return 55 | end 56 | local protocol_name, _ = extract_lp_hex_string(buffer) 57 | if(protocol_name == nil) then 58 | -- TODO: reassemble 59 | return 60 | end 61 | 62 | state.supported = protocol_name:sub(1, -2) == state.protocol 63 | state.handshaked = true 64 | state.ackPacketId = pinfo.number 65 | return 66 | end 67 | 68 | if(state.dialerMSver == nil) then 69 | -- select packet with protocol version 70 | if (packet_len < 21) then 71 | -- TODO: reassemble 72 | return 73 | end 74 | local protocol_name, bytes_len = extract_lp_hex_string(buffer) 75 | if(protocol_name == nil) then 76 | -- TODO: reassemble 77 | return 78 | end 79 | 80 | local req_protocol_name, tt = extract_lp_hex_string(buffer(bytes_len, -1)) 81 | if(req_protocol_name == nil) then 82 | -- TODO: reassemble 83 | return 84 | end 85 | 86 | state.dialerMSver = protocol_name:sub(1, -2) 87 | state.protocol = req_protocol_name:sub(1, -2) 88 | state.selectPacketId = pinfo.number 89 | end 90 | end 91 | 92 | -- this disssector should be called after the "multistream 1.0.0" string observed 93 | function multistream_proto.dissector (buffer, pinfo, tree) 94 | local state = MSStates:getState(pinfo) 95 | if (not state) then 96 | -- it is impossible to continue work without state 97 | print(string.format("multistream dissector: error while getting state on %s:%s - %s:%s", 98 | tostring(pinfo.src), 99 | tostring(pinfo.src_port), 100 | tostring(pinfo.dst), 101 | tostring(pinfo.dst_port) 102 | )) 103 | return 104 | end 105 | 106 | if (not state.handshaked) then 107 | dissect_handshake(buffer, pinfo, state) 108 | end 109 | 110 | local subtree = tree:add(multistream_proto, multistream_proto.description) 111 | local packet_len = buffer:len() 112 | pinfo.cols.protocol = multistream_proto.name 113 | pinfo.cols.info = "multistream" 114 | 115 | if (state.helloPacketId == pinfo.number) then 116 | pinfo.cols.info = string.format("%s ready (%s)", pinfo.cols.info, state.listenerMSver) 117 | subtree:add(fields.multistream_version, buffer(0, packet_len)):append_text(" (" .. state.listenerMSver .. ")") 118 | elseif (state.selectPacketId == pinfo.number) then 119 | pinfo.cols.info = string.format("%s ready (%s) select (%s)", pinfo.cols.info, state.dialerMSver, state.protocol) 120 | subtree:add(fields.multistream_version, buffer(0, 20)):append_text(" (" .. state.dialerMSver .. ")") 121 | subtree:add(fields.multistream_protocol, buffer(21, -1)):append_text(" (" .. state.protocol .. ")") 122 | elseif (state.ackPacketId == pinfo.number) then 123 | if(state.supported) then 124 | pinfo.cols.info = string.format("%s ACK (%s)", pinfo.cols.info, state.protocol) 125 | else 126 | pinfo.cols.info = string.format("%s NACK", pinfo.cols.info) 127 | end 128 | subtree:add(fields.multistream_protocol, buffer(0, packet_len)):append_text(" (" .. state.protocol .. ")") 129 | else 130 | if (state.protocol == "/secio/1.0.0") then 131 | subtree:add(fields.multistream_protocol, buffer(0, 0)):append_text(" (" .. state.protocol .. ")") 132 | 133 | Dissector.get("secio"):call(buffer, pinfo, tree) 134 | return 135 | end 136 | 137 | if(state.protocol ~= nil) then 138 | print(string.format("multistream dissector: %s protocol is unsuported", state.protocol)) 139 | else 140 | print("multistream dissector: underlying protocol is unsuported") 141 | end 142 | end 143 | end 144 | 145 | -- returns true if some packet contains a length-prefixed string "/multistream/1.0.0\n" 146 | local function m_heuristic_checker(buffer, pinfo, tree) 147 | local m_ready_packet_size = 0x14 148 | local packet_len = buffer:len() 149 | if packet_len < m_ready_packet_size then 150 | return false 151 | end 152 | 153 | local protocol_name, byte_count = extract_lp_hex_string(buffer) 154 | if(byte_count == 0 or protocol_name == nil) then 155 | return false 156 | end 157 | 158 | if protocol_name ~= "/multistream/1.0.0\n" then 159 | return false 160 | end 161 | 162 | tcp_table = DissectorTable.get ("tcp.port") 163 | tcp_table:add(pinfo.src_port, multistream_proto) 164 | tcp_table:add(pinfo.dst_port, multistream_proto) 165 | 166 | local m_state = MSStates:addNewState(pinfo) 167 | local s_state = SecioStates:addNewState(pinfo) 168 | 169 | if packet_len > m_ready_packet_size then 170 | set_address(m_state.dialer, pinfo.src, pinfo.src_port) 171 | set_address(m_state.listener, pinfo.dst, pinfo.dst_port) 172 | else 173 | set_address(m_state.listener, pinfo.src, pinfo.src_port) 174 | set_address(m_state.dialer, pinfo.dst, pinfo.dst_port) 175 | end 176 | SecioStates:init_with_mstate(s_state, m_state) 177 | 178 | print(string.format("multistream dissector: dissector for (listener %s:%s) - (dialer %s:%s) registered", 179 | tostring(m_state.listener.ip), 180 | tostring(m_state.listener.port), 181 | tostring(m_state.dialer.ip), 182 | tostring(m_state.dialer.port)) 183 | ) 184 | 185 | multistream_proto.dissector(buffer, pinfo, tree) 186 | return true 187 | end 188 | 189 | multistream_proto:register_heuristic("udp", m_heuristic_checker) 190 | multistream_proto:register_heuristic("tcp", m_heuristic_checker) 191 | -------------------------------------------------------------------------------- /src/protocols/multistream/multistream_state.lua: -------------------------------------------------------------------------------- 1 | -- prevent wireshark loading this file as a plugin 2 | if not _G['libp2p_dissector'] then return end 3 | require("net_addresses") 4 | 5 | local MSStates = {} 6 | 7 | local function initState(state) 8 | -- address of a listener peer 9 | state.listener = {} 10 | 11 | -- address of a dialer peer 12 | state.dialer = {} 13 | 14 | -- multistream protocol version of a listener peer 15 | state.listenerMSver = nil 16 | 17 | -- multistream protocol version of a dieler peer 18 | state.dialerMSver = nil 19 | 20 | -- synchronized protocol version 21 | state.protocol = nil 22 | 23 | -- true, if dialer supports proposal protocol 24 | state.supported = false 25 | 26 | -- number of a hello packet 27 | state.helloPacketId = -1 28 | 29 | -- number of a select packet 30 | state.selectPacketId = -1 31 | 32 | -- number of a ack packet 33 | state.ackPacketId = -1 34 | 35 | -- true, if all of hello, select and ack packets have been seen 36 | state.handshaked = false 37 | end 38 | 39 | function MSStates:addNewState(pinfo) 40 | -- check that there is already such state 41 | local key_1, key_2 = transform_pinfo_to_keys(pinfo) 42 | if self[key_1] ~= nil then 43 | return self[key_1] 44 | elseif self[key_2] ~= nil then 45 | return self[key_2] 46 | end 47 | self[key_1] = {} 48 | initState(self[key_1]) 49 | 50 | return self[key_1] 51 | end 52 | 53 | function MSStates:getState(pinfo) 54 | -- check that there is already such state 55 | local key_1, key_2 = transform_pinfo_to_keys(pinfo) 56 | if self[key_1] ~= nil then 57 | return self[key_1] 58 | end 59 | 60 | return self[key_2] 61 | end 62 | 63 | return MSStates 64 | -------------------------------------------------------------------------------- /src/protocols/secio/openssl_ffi.lua: -------------------------------------------------------------------------------- 1 | -- prevent wireshark loading this file as a plugin 2 | if not _G['libp2p_dissector'] then return end 3 | 4 | local ffi = require("ffi") 5 | 6 | -- https://stackoverflow.com/questions/35557928/ffi-encryption-decryption-with-luajit 7 | ffi.cdef [[ 8 | typedef struct evp_cipher_st EVP_CIPHER; 9 | typedef struct evp_cipher_ctx_st EVP_CIPHER_CTX; 10 | typedef struct engine_st ENGINE; 11 | 12 | EVP_CIPHER_CTX *EVP_CIPHER_CTX_new(void); 13 | int EVP_CIPHER_CTX_reset(EVP_CIPHER_CTX *ctx); 14 | void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *ctx); 15 | 16 | int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, 17 | ENGINE *impl, const unsigned char *key, const unsigned char *iv); 18 | int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, 19 | int *outl, const unsigned char *in, int inl); 20 | int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out, 21 | int *outl); 22 | 23 | int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, 24 | ENGINE *impl, const unsigned char *key, const unsigned char *iv); 25 | int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, 26 | int *outl, const unsigned char *in, int inl); 27 | int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm, int *outl); 28 | 29 | 30 | const EVP_CIPHER *EVP_aes_128_ctr(void); 31 | const EVP_CIPHER *EVP_aes_256_ctr(void); 32 | ]] 33 | -------------------------------------------------------------------------------- /src/protocols/secio/proto.lua: -------------------------------------------------------------------------------- 1 | -- prevent wireshark loading this file as a plugin 2 | if not _G['libp2p_dissector'] then return end 3 | 4 | local protoc = require ("protoc") 5 | 6 | assert(protoc:load [[ 7 | message Propose { 8 | optional bytes rand = 1; 9 | optional bytes pubkey = 2; 10 | optional string exchanges = 3; 11 | optional string ciphers = 4; 12 | optional string hashes = 5; 13 | } 14 | 15 | message Exchange { 16 | optional bytes epubkey = 1; 17 | optional bytes signature = 2; 18 | } ]] 19 | ) 20 | -------------------------------------------------------------------------------- /src/protocols/secio/secio.lua: -------------------------------------------------------------------------------- 1 | -- prevent wireshark loading this file as a plugin 2 | if not _G['libp2p_dissector'] then return end 3 | 4 | local pb = require ("pb") 5 | local SecioStates = require ("secio_state") 6 | local utils = require ("secio_misc") 7 | 8 | secio_proto = Proto("secio", "SECIO protocol") 9 | 10 | local fields = secio_proto.fields 11 | 12 | -- field related to secio packet size 13 | fields.packet_len = ProtoField.uint32 ("secio.packet_size", "Packet size", base.HEX, nil, 0, "Secio packet size in bytes") 14 | 15 | -- fields related to Propose packets type 16 | fields.propose = ProtoField.bytes ("secio.propose", "Propose", base.NONE, nil, 0, "Propose request") 17 | fields.rand = ProtoField.bytes ("secio.propose.rand", "rand", base.NONE, nil, 0, "Propose random bytes") 18 | fields.pubkey = ProtoField.bytes ("secio.propose.pubkey", "pubkey", base.NONE, nil, 0, "Propose public key") 19 | fields.exchanges = ProtoField.string ("secio.propose.exchanges", "exchanges", base.NONE, nil, 0, "Propose exchanges") 20 | fields.ciphers = ProtoField.string ("secio.propose.ciphers", "ciphers", base.NONE, nil, 0, "Propose ciphers") 21 | fields.hashes = ProtoField.string ("secio.propose.hashes", "hashes", base.NONE, nil, 0, "Propose hashes") 22 | 23 | -- fields related to Exchange packets type 24 | fields.exchange = ProtoField.bytes ("secio.exchange", "exchange", base.NONE, nil, 0, "Exchange request") 25 | fields.epubkey = ProtoField.bytes ("secio.exchange.epubkey", "epubkey", base.NONE, nil, 0, "Ephermal public key") 26 | fields.signature = ProtoField.bytes ("secio.exchange.signature", "signature", base.NONE, nil, 0, "Exchange signature") 27 | 28 | -- fields related to Body packets type 29 | fields.cipher_text = ProtoField.bytes ("secio.body.cipher_text", "cipher text", base.NONE, nil, 0, "Cipher text") 30 | fields.hmac = ProtoField.bytes ("secio.body.hmac", "HMAC", base.NONE, nil, 0, "HMAC of cipher text") 31 | 32 | local function dissect_handshake(buffer, pinfo, state) 33 | local is_listener = false 34 | 35 | -- heuristic multistream detector should already set MSState.listener and MSState.dialer fields 36 | if (is_same_src_address(state.listener, pinfo)) then 37 | is_listener = true 38 | elseif (not is_same_src_address(state.dialer, pinfo)) then 39 | -- some error occured 40 | print("multistream dissector: ip:port are incorrect") 41 | return 42 | end 43 | 44 | if(is_listener) then 45 | if (state.listenerProposePacketId == -1) then 46 | state.listenerProposePacketId = pinfo.number 47 | elseif (state.listenerExchangePacketId == -1) then 48 | state.listenerExchangePacketId = pinfo.number 49 | end 50 | else 51 | if (state.dialerProposePacketId == -1) then 52 | state.dialerProposePacketId = pinfo.number 53 | elseif (state.dialerExchangePacketId == -1) then 54 | state.dialerExchangePacketId = pinfo.number 55 | end 56 | end 57 | 58 | if ( 59 | state.listenerProposePacketId ~= -1 and 60 | state.dialerProposePacketId ~= -1 and 61 | state.listenerExchangePacketId ~= -1 and 62 | state.dialerExchangePacketId ~= -1 63 | ) then 64 | state.handshaked = true 65 | end 66 | end 67 | 68 | local function parse_and_set_propose(buffer, tree) 69 | tree:add(fields.packet_len, buffer(0, 4)) 70 | local branch = tree:add("Propose", fields.propose) 71 | 72 | local propose = assert(pb.decode("Propose", buffer:raw(4, -1))) 73 | local offset = 4 74 | 75 | -- check for fields presence and add them to the tree 76 | if (propose.rand ~= nil) then 77 | branch:add(fields.rand, buffer(offset, propose.rand:len() + 3)) 78 | offset = offset + propose.rand:len() + 3 79 | end 80 | 81 | if (propose.pubkey ~= nil) then 82 | branch:add(fields.pubkey, buffer(offset, propose.pubkey:len() + 4)) 83 | offset = offset + propose.pubkey:len() + 4 84 | end 85 | 86 | if (propose.exchanges ~= nil) then 87 | branch:add(fields.exchanges, buffer(offset, propose.exchanges:len())) 88 | offset = offset + propose.exchanges:len() 89 | end 90 | 91 | if (propose.ciphers ~= nil) then 92 | branch:add(fields.ciphers, buffer(offset + 2, propose.ciphers:len())) 93 | offset = offset + propose.ciphers:len() 94 | end 95 | 96 | if (propose.hashes ~= nil) then 97 | branch:add(fields.hashes, buffer(offset + 4, propose.hashes:len())) 98 | offset = offset + propose.hashes:len() 99 | end 100 | end 101 | 102 | local function parse_and_set_exchange(buffer, tree) 103 | tree:add(fields.packet_len, buffer(0, 4)) 104 | local branch = tree:add("Exchange", fields.exchange) 105 | 106 | local exchange = assert(pb.decode("Exchange", buffer:raw(4, -1))) 107 | local offset = 4 108 | 109 | -- check for fields presence and add them to the tree 110 | if (exchange.epubkey ~= nil) then 111 | branch:add(fields.epubkey, buffer(offset, exchange.epubkey:len() + 2)) 112 | offset = offset + exchange.epubkey:len() + 2 113 | end 114 | 115 | if (exchange.signature ~= nil) then 116 | branch:add(fields.signature, buffer(offset, exchange.signature:len() + 2)) 117 | offset = offset + exchange.signature:len() + 2 118 | end 119 | end 120 | 121 | function secio_proto.dissector (buffer, pinfo, tree) 122 | -- the message should be at least 4 bytes 123 | if buffer:len() < 4 then 124 | return 125 | end 126 | 127 | local state = SecioStates:getState(pinfo) 128 | 129 | if (not state or next(state.listener) == nil) then 130 | -- it is impossible to continue work without state 131 | print(string.format("secio dissector: error while getting state on %s:%s - %s:%s", 132 | tostring(pinfo.src), 133 | tostring(pinfo.src_port), 134 | tostring(pinfo.dst), 135 | tostring(pinfo.dst_port) 136 | )) 137 | return 138 | end 139 | 140 | local subtree = tree:add(secio_proto, "SECIO protocol") 141 | pinfo.cols.protocol = secio_proto.name 142 | 143 | if (not state.handshaked) then 144 | dissect_handshake(buffer, pinfo, state) 145 | end 146 | 147 | -- according to the spec, first 4 bytes always represent packet size 148 | local packet_len = buffer(0, 4):uint() 149 | 150 | if (state.listenerProposePacketId == pinfo.number) then 151 | pinfo.cols.info = "SECIO: Propose (listener)" 152 | parse_and_set_propose(buffer, subtree) 153 | elseif (state.dialerProposePacketId == pinfo.number) then 154 | pinfo.cols.info = "SECIO: Propose (dialer)" 155 | parse_and_set_propose(buffer, subtree) 156 | elseif (state.listenerExchangePacketId == pinfo.number) then 157 | pinfo.cols.info = "SECIO Exchange (listener)" 158 | parse_and_set_exchange(buffer, subtree) 159 | elseif (state.dialerExchangePacketId == pinfo.number) then 160 | pinfo.cols.info = "SECIO Exchange (dialer)" 161 | parse_and_set_exchange(buffer, subtree) 162 | elseif (state.handshaked) then 163 | -- encrypted packets 164 | 165 | if (next(state.cryptoParams) == nil) then 166 | SecioStates:init_crypto_params(state, pinfo) 167 | end 168 | 169 | pinfo.cols.info = "SECIO Body" 170 | local plain_text = "" 171 | local hmac_type = state.listenerHMACType 172 | local hmac_size = utils:hashSize(state.listenerHMACType) 173 | 174 | -- if see this packet for the first time, we need to decrypt it 175 | if not pinfo.visited then 176 | -- [4 bytes len][ cipher_text ][ H(cipher_text) ] 177 | if (is_same_src_address(state.listener, pinfo)) then 178 | plain_text = state.listenerMsgDecryptor(buffer:raw(4, packet_len - hmac_size)) 179 | else 180 | hmac_type = state.dialerHMACType 181 | hmac_size = utils:hashSize(state.dialerHMACType) 182 | plain_text = state.dialerMsgDecryptor(buffer:raw(4, packet_len - hmac_size)) 183 | end 184 | 185 | state.decryptedPayloads[pinfo.number] = plain_text 186 | else 187 | plain_text = state.decryptedPayloads[pinfo.number] 188 | end 189 | 190 | local offset = 0 191 | subtree:add(fields.packet_len, buffer(offset, 4)) 192 | offset = offset + 4 193 | 194 | plain_text = Struct.tohex(tostring(plain_text)) 195 | subtree:add(buffer(offset, packet_len - hmac_size), 196 | string.format("cipher text 0x%X bytes: (plain text is %s )", #plain_text, plain_text) 197 | ) 198 | offset = offset + packet_len - hmac_size 199 | 200 | subtree:add(fields.hmac, buffer(offset, -1)):append_text(string.format("(%s)", hmac_type)) 201 | 202 | pinfo.private["plain_text"] = plain_text 203 | Dissector.get("mplex"):call(buffer, pinfo, tree) 204 | end 205 | end 206 | 207 | return secio_proto 208 | -------------------------------------------------------------------------------- /src/protocols/secio/secio_misc.lua: -------------------------------------------------------------------------------- 1 | -- prevent wireshark loading this file as a plugin 2 | if not _G['libp2p_dissector'] then return end 3 | 4 | require("openssl_ffi") 5 | local ffi = require("ffi") 6 | local C = ffi.C 7 | ffi.load("ssl") 8 | 9 | local Utils = {} 10 | 11 | -- returns lamdba that can decrypt SECIO messages based on the provided cipher settings 12 | function Utils:makeMsgDecryptor(cipher_type, key, iv) 13 | -- returns lamdba that can decrypt raw cipher text based on the provided parameters 14 | local function makeDecryptor(cipher_type, key, iv) 15 | -- initialize the cipher context 16 | local ctx = C.EVP_CIPHER_CTX_new() 17 | assert(ctx ~= nil) 18 | 19 | -- set up the cipher type 20 | if "AES-128" == cipher_type then 21 | -- EVP_DecryptInit_ex returns 1 if succeed and 0 otherwise 22 | assert(1 == C.EVP_DecryptInit_ex(ctx, C.EVP_aes_128_ctr(), nil, key, iv)) 23 | elseif "AES-256" == cipher_type then 24 | -- EVP_DecryptInit_ex returns 1 if succeed and 0 otherwise 25 | assert(1 == C.EVP_DecryptInit_ex(ctx, C.EVP_aes_256_ctr(), nil, key, iv)) 26 | else 27 | error('Unsupported cipher type: ' .. cipher_type .. ". At now, only AES-128, AES-256 are supported") 28 | end 29 | 30 | -- return lamdba that can decrypt supplied cipher text 31 | return function(cipher_text) 32 | if not cipher_text then 33 | print("EVP_CIPHER_CTX_free") 34 | C.EVP_CIPHER_CTX_free(ctx) 35 | return 36 | end 37 | 38 | local cipher_text_len = #cipher_text 39 | local plain_text = ffi.new("unsigned char[?]", cipher_text_len) 40 | local ffi_len = ffi.new 'int[1]' 41 | assert(1 == C.EVP_DecryptUpdate(ctx, plain_text, ffi_len, cipher_text, cipher_text_len)) 42 | return ffi.string(plain_text, ffi_len[0]) 43 | end 44 | end 45 | 46 | local decryptor = makeDecryptor(cipher_type, key, iv) 47 | 48 | -- return lamdba that can decrypt supplied message 49 | return function (msg) 50 | --print("enc len " .. #msg) 51 | return decryptor(msg) 52 | end 53 | end 54 | 55 | function Utils:hashSize(hmac_type) 56 | if "SHA256" == hmac_type then 57 | return 32 58 | elseif "SHA512" == hmac_type then 59 | return 64 60 | else 61 | error('Unsupported HMAC type: ' .. hmac_type .. ". At now, only SHA256, SHA512 are supported") 62 | end 63 | end 64 | 65 | return Utils 66 | -------------------------------------------------------------------------------- /src/protocols/secio/secio_state.lua: -------------------------------------------------------------------------------- 1 | -- prevent wireshark loading this file as a plugin 2 | if not _G['libp2p_dissector'] then return end 3 | 4 | local config = require ("config") 5 | local utils = require ("secio_misc") 6 | require("net_addresses") 7 | 8 | local SecioStates = {} 9 | 10 | local function initState(state) 11 | -- address of a listener peer 12 | state.listener = {} 13 | 14 | -- address of a dialer peer 15 | state.dialer = {} 16 | 17 | -- packet id of a listener peer propose packet 18 | state.listenerProposePacketId = -1 19 | 20 | -- packet id of a dialer peer propose packet 21 | state.dialerProposePacketId = -1 22 | 23 | -- packet id of a listener peer exchange packet 24 | state.listenerExchangePacketId = -1 25 | 26 | -- packet id of a dialer peer exchange packet 27 | state.dialerExchangePacketId = -1 28 | 29 | -- table packet_num -> plain_text 30 | state.decryptedPayloads = {} 31 | 32 | -- size of listener HMAC in bytes 33 | state.listenerHMACType = nil 34 | 35 | -- size of dialer HMAC in bytes 36 | state.dialerHMACType = nil 37 | 38 | -- lambda that can decrypt listener messages 39 | state.listenerMsgDecryptor = nil 40 | 41 | -- lambda that can decrypt dialer messages 42 | state.dialerMsgDecryptor = nil 43 | 44 | -- table contains different crypto parameters from the config file 45 | state.cryptoParams = {} 46 | 47 | -- true, if propose and exchange packets both from listener and dialer have been seen 48 | state.handshaked = false 49 | end 50 | 51 | function SecioStates:addNewState(pinfo) 52 | -- check that there is already such state 53 | local key_1, key_2 = transform_pinfo_to_keys(pinfo) 54 | if self[key_1] ~= nil then 55 | return self[key_1] 56 | elseif self[key_2] ~= nil then 57 | return self[key_2] 58 | end 59 | self[key_1] = {} 60 | initState(self[key_1]) 61 | 62 | return self[key_1] 63 | end 64 | 65 | function SecioStates:getState(pinfo) 66 | -- check that there is already such state 67 | local key_1, key_2 = transform_pinfo_to_keys(pinfo) 68 | if self[key_1] ~= nil then 69 | return self[key_1] 70 | end 71 | 72 | return self[key_2] 73 | end 74 | 75 | function SecioStates:init_with_mstate(state, mstate) 76 | state.listener["ip"] = mstate.listener["ip"] 77 | state.listener["port"] = mstate.listener["port"] 78 | state.dialer["ip"] = mstate.dialer["ip"] 79 | state.dialer["port"] = mstate.dialer["port"] 80 | end 81 | 82 | function SecioStates:init_crypto_params(state, pinfo) 83 | state.cryptoParams = config:load_config_for(pinfo.src, pinfo.src_port, pinfo.dst, pinfo.dst_port) 84 | assert(next(state.cryptoParams) ~= nil, "secio dissector: error while reading config file") 85 | 86 | state.listenerHMACType = state.cryptoParams.local_hmac_type 87 | state.dialerHMACType = state.cryptoParams.remote_hmac_type 88 | state.listenerMsgDecryptor = utils:makeMsgDecryptor( 89 | state.cryptoParams.local_cipher_type, 90 | state.cryptoParams.local_key, 91 | state.cryptoParams.local_iv 92 | ) 93 | state.dialerMsgDecryptor = utils:makeMsgDecryptor( 94 | state.cryptoParams.remote_cipher_type, 95 | state.cryptoParams.remote_key, 96 | state.cryptoParams.remote_iv 97 | ) 98 | end 99 | 100 | return SecioStates 101 | -------------------------------------------------------------------------------- /src/utils/config.lua: -------------------------------------------------------------------------------- 1 | -- prevent wireshark loading this file as a plugin 2 | if not _G['libp2p_dissector'] then return end 3 | 4 | local csv = require("csv") 5 | local base64 = require ("base64") 6 | 7 | local Config = {} 8 | 9 | function Config:load_config_for(src_ip, src_port, dst_ip, dst_port) 10 | -- the env variable already checked on the plugin loading stage 11 | local key_file_path = os.getenv("LIBP2P_SECIO_KEYLOG") 12 | local key_file = csv.open(key_file_path, {separator = ",", header = true}) 13 | if(key_file == nil) then 14 | print("libp2p dissector: config reading error") 15 | return nil 16 | end 17 | 18 | local src_addr = string.format("%s:%s", tostring(src_ip), tostring(src_port)) 19 | local dst_addr = string.format("%s:%s", tostring(dst_ip), tostring(dst_port)) 20 | 21 | local result_record = {} 22 | for record in key_file:lines() do 23 | if( 24 | (record["local_addr"] == src_addr and record["remote_addr"] == dst_addr) or 25 | (record["remote_addr"] == src_addr and record["local_addr"] == dst_addr) 26 | ) then 27 | result_record = record 28 | end 29 | end 30 | 31 | if next(result_record) == nil then 32 | return result_record 33 | end 34 | 35 | result_record["local_key"] = base64.decode(result_record["local_key"]) 36 | result_record["local_iv"] = base64.decode(result_record["local_iv"]) 37 | result_record["local_mac"] = base64.decode(result_record["local_mac"]) 38 | result_record["remote_key"] = base64.decode(result_record["remote_key"]) 39 | result_record["remote_iv"] = base64.decode(result_record["remote_iv"]) 40 | result_record["remote_mac"] = base64.decode(result_record["remote_mac"]) 41 | 42 | return result_record 43 | end 44 | 45 | return Config 46 | -------------------------------------------------------------------------------- /src/utils/length-prefixed.lua: -------------------------------------------------------------------------------- 1 | -- prevent wireshark loading this file as a plugin 2 | if not _G['libp2p_dissector'] then return end 3 | 4 | require("uvarint") 5 | 6 | local VARIANT_MAX_LEN = 8 7 | 8 | -- extracts length prefixed buffer from the given buffer 9 | function extract_lp(buffer) 10 | local string_len, bytes_count = extractUvarint(buffer, VARIANT_MAX_LEN) 11 | if (string_len == nil) then 12 | -- return both buffer and resulted varint for debug purpouses 13 | return nil, bytes_count 14 | end 15 | 16 | if (buffer:len() < bytes_count + string_len) then 17 | -- TODO: check variant with reassembling 18 | return nil, bytes_count 19 | end 20 | 21 | return buffer(bytes_count, string_len), bytes_count + string_len 22 | end 23 | 24 | -- extracts length prefixed string from the given buffer 25 | function extract_lp_string(buffer) 26 | local extracted_lp, bytes_count = extract_lp(buffer) 27 | return tostring(extracted_lp), bytes_count 28 | end 29 | 30 | -- extracts length prefixed hex string from the given buffer 31 | function extract_lp_hex_string(buffer) 32 | local function fromhex(str) 33 | return (str:gsub('..', function (cc) 34 | return string.char(tonumber(cc, 16)) 35 | end)) 36 | end 37 | 38 | local extracted_lp, bytes_count = extract_lp(buffer) 39 | return fromhex(tostring(extracted_lp)), bytes_count 40 | end 41 | -------------------------------------------------------------------------------- /src/utils/net_addresses.lua: -------------------------------------------------------------------------------- 1 | -- prevent wireshark loading this file as a plugin 2 | if not _G['libp2p_dissector'] then return end 3 | 4 | function is_same_addresses(table, ip, port) 5 | return next(table) ~= nil and table["ip"] == tostring(ip) and table["port"] == tostring(port) 6 | end 7 | 8 | function is_same_src_address(table, pinfo) 9 | return is_same_addresses(table, pinfo.src, pinfo.src_port) 10 | end 11 | 12 | function is_same_dst_address(table, pinfo) 13 | return is_same_addresses(table, pinfo.dst, pinfo.dst_port) 14 | end 15 | 16 | function set_address(table, ip, port) 17 | table["ip"] = tostring(ip) 18 | table["port"] = tostring(port) 19 | end 20 | 21 | function transform_pinfo_to_keys(pinfo) 22 | local key_1 = string.format("%s:%s:%s:%s", pinfo.src, pinfo.src_port, pinfo.dst, pinfo.dst_port) 23 | local key_2 = string.format("%s:%s:%s:%s", pinfo.dst, pinfo.dst_port, pinfo.src, pinfo.src_port) 24 | return key_1, key_2 25 | end 26 | -------------------------------------------------------------------------------- /src/utils/table_copy.lua: -------------------------------------------------------------------------------- 1 | -- prevent wireshark loading this file as a plugin 2 | if not _G['libp2p_dissector'] then return end 3 | 4 | local t = {} 5 | 6 | --http://lua-users.org/wiki/CopyTable 7 | -- Save copied tables in `copies`, indexed by original table. 8 | function t:table_copy(orig, copies) 9 | copies = copies or {} 10 | local orig_type = type(orig) 11 | local copy 12 | if orig_type == 'table' then 13 | if copies[orig] then 14 | copy = copies[orig] 15 | else 16 | copy = {} 17 | copies[orig] = copy 18 | setmetatable(copy, deepcopy(getmetatable(orig), copies)) 19 | for orig_key, orig_value in next, orig, nil do 20 | copy[deepcopy(orig_key, copies)] = deepcopy(orig_value, copies) 21 | end 22 | end 23 | else -- number, string, boolean, etc 24 | copy = orig 25 | end 26 | return copy 27 | end 28 | 29 | return t 30 | -------------------------------------------------------------------------------- /src/utils/uvarint.lua: -------------------------------------------------------------------------------- 1 | -- prevent wireshark loading this file as a plugin 2 | if not _G['libp2p_dissector'] then return end 3 | 4 | -- TODO: maybe make it a separate lib? 5 | 6 | local bit = require ("bit32") 7 | 8 | local bor = bit.bor 9 | local band = bit.band 10 | local lshift = bit.lshift 11 | 12 | -- extracts uvarint from given byte stream 13 | -- returns uvarint and read bytes count 14 | function extractUvarint(byte_stream, max_len) 15 | local result = 0 16 | 17 | for offset=0, max_len do 18 | local byte = byte_stream(offset, 1):uint() 19 | local cut_byte = band(byte, 0x7F) 20 | result = bor(result, lshift(cut_byte, offset * 7)) 21 | if byte < 0x80 then 22 | return result, offset + 1 23 | end 24 | end 25 | 26 | -- max byte count handled, but a byte with unset msb hasn't met 27 | return nil, 0 28 | end 29 | --------------------------------------------------------------------------------