|
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
|
|
|