Project

General

Profile

Bug #5766 » packet_log.lua

Alex Lasky, 12/23/2022 06:24 AM

 
local bit = require("bit")
local strbuf = require("string.buffer")
frag_buf = strbuf.new(2048)
prev_frame_num = nil

function init (args)
local needs = {}
needs["payload"] = tostring(true)
return needs
end --function init


--Reassembles DNP3 fragment (stored in frag_buf) from individual DNP3 frames.
--Returns true only when fragment is complete
function reassembled(pkt)
local fin_frame
repeat
local frame_len_byte = pkt:byte(3)
local crc_bytes = 2*math.ceil( (frame_len_byte-5)/16 ) + 2
local frame_len = 3 + frame_len_byte + crc_bytes
local transport_byte = pkt:byte(11)
fin_frame = transport_byte >= 128
local fir_frame = bit.band(transport_byte,64) > 0
local frame_num = bit.band(transport_byte,63)
local frame_payload = pkt:sub(11,frame_len-2) --Remove final CRC bytes & DLL header
frame_payload = frame_payload:gsub( "(" .. string.rep('.',16) .. ")..", "%1" ) --Remove other CRC's
frame_payload = frame_payload:sub(2) --Remove transport byte

if fir_frame then
frag_buf:set(frame_payload)
prev_frame_num = frame_num
elseif prev_frame_num and (prev_frame_num + 1) % 64 == frame_num then
frag_buf:put(frame_payload)
prev_frame_num = frame_num
end
pkt = pkt:sub(frame_len+1)
until #pkt < 12
return fin_frame
end --function reassembled


--Creates logged DNP3 files from g70v1 DNP3 download/append request fragments
function log_dnp3_file(frag)
for i,v in pairs({[3]=70,[4]=1}) do
if frag:byte(i) ~= v then return 0 end
end --Check that this is a g70v1 request fragment

local file_func_code = frag:byte(40)
if file_func_code ~= 0 and file_func_code ~= 3 then return 0 end
local name_len = frag:byte(9)*256 + frag:byte(8)
local dnp3_name = frag:sub(42, 41 + name_len)

local offset = 42+name_len; local content,record_size = ""
repeat
record_size = frag:byte(offset+1)*256 + frag:byte(offset)
content = content .. frag:sub(offset+2,offset+1+record_size)
offset = offset + record_size + 2
until offset+2 > #frag --"+2" prevents runtime err on empty record

if 3 == file_func_code or dnp3_name ~= initial_dnp3_name then --New file download started
downloaded_byte_cnt = 0; file_size = 0; rec = 0
local new_tab = require "table.new"
file_table = new_tab( math.ceil(file_size/1800), 0 )
initial_buf_len = #frag
initial_dnp3_name = dnp3_name
for i=19,16,-1 do file_size = file_size*256+frag:byte(i) end
local ip_version, src_ip, dst_ip, protocol, src_port, dst_port = SCFlowTuple()
log_filename = SCLogPath() .. '/' .. dst_ip .. os.date('!_%F_%T_') .. dnp3_name
end

downloaded_byte_cnt = downloaded_byte_cnt + #content
rec = rec + 1; file_table[rec] = content
if downloaded_byte_cnt >= file_size or #frag < initial_buf_len then
local file = assert( io.open(log_filename, 'w'), 'Failed to open ' .. log_filename )
file:write( table.concat(file_table) )
file:flush()
end
end --function log_dnp3_file


function match(args)
local pkt = args.payload --DNP3 frames
if #pkt < 12 then return 0 end --Minimum valid payload-containing frame length is 12
for i,v in pairs({[1]=5,[2]=0x64,[5]=0x52,[6]=0xc3,[7]=5,[8]=0}) do
if pkt:byte(i) ~= v then return 0 end
end --Confirm TCP payload is aligned to frame start and is sent from addr 5 to addr 50002

if not reassembled(pkt) then return 0 end

log_dnp3_file( frag_buf:get() )
return 1
end --function match
(3-3/3)