273 lines
10 KiB
Lua
273 lines
10 KiB
Lua
---
|
|
-- DICOM library
|
|
--
|
|
-- This library implements (partially) the DICOM protocol. This protocol is used to
|
|
-- capture, store and distribute medical images.
|
|
--
|
|
-- From Wikipedia:
|
|
-- The core application of the DICOM standard is to capture, store and distribute
|
|
-- medical images. The standard also provides services related to imaging such as
|
|
-- managing imaging procedure worklists, printing images on film or digital media
|
|
-- like DVDs, reporting procedure status like completion of an imaging acquisition,
|
|
-- confirming successful archiving of images, encrypting datasets, removing patient
|
|
-- identifying information from datasets, organizing layouts of images for review,
|
|
-- saving image manipulations and annotations, calibrating image displays, encoding
|
|
-- ECGs, encoding CAD results, encoding structured measurement data, and storing
|
|
-- acquisition protocols.
|
|
--
|
|
-- OPTIONS:
|
|
-- *<code>called_aet</code> - If set it changes the called Application Entity Title
|
|
-- used in the requests. Default: ANY-SCP
|
|
-- *<code>calling_aet</code> - If set it changes the calling Application Entity Title
|
|
-- used in the requests. Default: ECHOSCU
|
|
--
|
|
-- @args dicom.called_aet Called Application Entity Title. Default: ANY-SCP
|
|
-- @args dicom.calling_aet Calling Application Entity Title. Default: ECHOSCU
|
|
--
|
|
-- @author Paulino Calderon <paulino@calderonpale.com>
|
|
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
|
|
---
|
|
|
|
local nmap = require "nmap"
|
|
local stdnse = require "stdnse"
|
|
local string = require "string"
|
|
local table = require "table"
|
|
|
|
_ENV = stdnse.module("dicom", stdnse.seeall)
|
|
|
|
local MIN_SIZE_ASSOC_REQ = 68
|
|
local MAX_SIZE_PDU = 128000
|
|
local MIN_HEADER_LEN = 6
|
|
local PDU_NAMES = {}
|
|
local PDU_CODES = {}
|
|
|
|
PDU_CODES =
|
|
{
|
|
ASSOCIATE_REQUEST = 0x01,
|
|
ASSOCIATE_ACCEPT = 0x02,
|
|
ASSOCIATE_REJECT = 0x03,
|
|
DATA = 0x04,
|
|
RELEASE_REQUEST = 0x05,
|
|
RELEASE_RESPONSE = 0x06,
|
|
ABORT = 0x07
|
|
}
|
|
|
|
for i, v in pairs(PDU_CODES) do
|
|
PDU_NAMES[v] = i
|
|
end
|
|
|
|
---
|
|
-- start_connection(host, port) starts socket to DICOM service
|
|
--
|
|
-- @param host Host object
|
|
-- @param port Port table
|
|
-- @return (status, socket) If status is true, socket of DICOM object is set.
|
|
-- If status is false, socket is the error message.
|
|
---
|
|
function start_connection(host, port)
|
|
local dcm = {}
|
|
local status, err
|
|
dcm['socket'] = nmap.new_socket()
|
|
|
|
status, err = dcm['socket']:connect(host, port, "tcp")
|
|
|
|
if(status == false) then
|
|
return false, "DICOM: Failed to connect to host: " .. err
|
|
end
|
|
|
|
return true, dcm
|
|
end
|
|
|
|
---
|
|
-- send(dcm, data) Sends DICOM packet over established socket
|
|
--
|
|
-- @param dcm DICOM object
|
|
-- @param data Data to send
|
|
-- @return status True if data was sent correctly, otherwise false and error message is returned.
|
|
---
|
|
function send(dcm, data)
|
|
local status, err
|
|
stdnse.debug2("DICOM: Sending DICOM packet (%d)", #data)
|
|
if dcm['socket'] then
|
|
status, err = dcm['socket']:send(data)
|
|
if status == false then
|
|
return false, err
|
|
end
|
|
else
|
|
return false, "No socket found. Check your DICOM object"
|
|
end
|
|
return true
|
|
end
|
|
|
|
---
|
|
-- receive(dcm) Reads DICOM packets over an established socket
|
|
--
|
|
-- @param dcm DICOM object
|
|
-- @return (status, data) Returns data if status true, otherwise data is the error message.
|
|
---
|
|
function receive(dcm)
|
|
local status, data = dcm['socket']:receive()
|
|
if status == false then
|
|
return false, data
|
|
end
|
|
stdnse.debug1("DICOM: receive() read %d bytes", #data)
|
|
return true, data
|
|
end
|
|
|
|
---
|
|
-- pdu_header_encode(pdu_type, length) encodes the DICOM PDU header
|
|
--
|
|
-- @param pdu_type PDU type as ann unsigned integer
|
|
-- @param length Length of the DICOM message
|
|
-- @return (status, dcm) If status is true, the DICOM object with the header set is returned.
|
|
-- If status is false, dcm is the error message.
|
|
---
|
|
function pdu_header_encode(pdu_type, length)
|
|
-- Some simple sanity checks, we do not check ranges to allow users to create malformed packets.
|
|
if not(type(pdu_type)) == "number" then
|
|
return false, "PDU Type must be an unsigned integer. Range:0-7"
|
|
end
|
|
if not(type(length)) == "number" then
|
|
return false, "Length must be an unsigned integer."
|
|
end
|
|
|
|
local header = string.pack("<B >B I4",
|
|
pdu_type, -- PDU Type ( 1 byte - unsigned integer in Big Endian )
|
|
0, -- Reserved section ( 1 byte that should be set to 0x0 )
|
|
length) -- PDU Length ( 4 bytes - unsigned integer in Little Endian)
|
|
if #header < MIN_HEADER_LEN then
|
|
return false, "Header must be at least 6 bytes. Something went wrong."
|
|
end
|
|
return true, header
|
|
end
|
|
|
|
---
|
|
-- associate(host, port) Attempts to associate to a DICOM Service Provider by sending an A-ASSOCIATE request.
|
|
--
|
|
-- @param host Host object
|
|
-- @param port Port object
|
|
-- @return (status, dcm) If status is true, the DICOM object is returned.
|
|
-- If status is false, dcm is the error message.
|
|
---
|
|
|
|
function associate(host, port, calling_aet, called_aet)
|
|
local application_context = ""
|
|
local presentation_context = ""
|
|
local userinfo_context = ""
|
|
|
|
local status, dcm = start_connection(host, port)
|
|
if status == false then
|
|
return false, dcm
|
|
end
|
|
|
|
local application_context_name = "1.2.840.10008.3.1.1.1"
|
|
application_context = string.pack(">B B I2 c" .. #application_context_name,
|
|
0x10,
|
|
0x0,
|
|
#application_context_name,
|
|
application_context_name)
|
|
|
|
local abstract_syntax_name = "1.2.840.10008.1.1"
|
|
local transfer_syntax_name = "1.2.840.10008.1.2"
|
|
presentation_context = string.pack(">B B I2 B B B B B B I2 c" .. #abstract_syntax_name .. "B B I2 c".. #transfer_syntax_name,
|
|
0x20, -- Presentation context type ( 1 byte )
|
|
0x0, -- Reserved ( 1 byte )
|
|
0x2e, -- Item Length ( 2 bytes )
|
|
0x1, -- Presentation context id ( 1 byte )
|
|
0x0,0x0,0x0, -- Reserved ( 3 bytes )
|
|
0x30, -- Abstract Syntax Tree ( 1 byte )
|
|
0x0, -- Reserved ( 1 byte )
|
|
0x11, -- Item Length ( 2 bytes )
|
|
abstract_syntax_name,
|
|
0x40, -- Transfer Syntax ( 1 byte )
|
|
0x0, -- Reserved ( 1 byte )
|
|
0x11, -- Item Length ( 2 bytes )
|
|
transfer_syntax_name)
|
|
|
|
local implementation_id = "1.2.276.0.7230010.3.0.3.6.2"
|
|
local implementation_version = "OFFIS_DCMTK_362"
|
|
userinfo_context = string.pack(">B B I2 B B I2 I4 B B I2 c" .. #implementation_id .. " B B I2 c".. #implementation_version,
|
|
0x50, -- Type 0x50 (1 byte)
|
|
0x0, -- Reserved ( 1 byte )
|
|
0x3a, -- Length ( 2 bytes )
|
|
0x51, -- Type 0x51 ( 1 byte)
|
|
0x0, -- Reserved ( 1 byte)
|
|
0x04, -- Length ( 2 bytes )
|
|
0x4000, -- DATA ( 4 bytes )
|
|
0x52, -- Type 0x52 (1 byte)
|
|
0x0,
|
|
0x1b,
|
|
implementation_id,
|
|
0x55,
|
|
0x0,
|
|
0x0f,
|
|
implementation_version)
|
|
|
|
local called_ae_title = called_aet or stdnse.get_script_args("dicom.called_aet") or "ANY-SCP"
|
|
local calling_ae_title = calling_aet or stdnse.get_script_args("dicom.calling_aet") or "ECHOSCU"
|
|
if #called_ae_title > 16 or #calling_ae_title > 16 then
|
|
return false, "Calling/Called Application Entity Title must be less than 16 bytes"
|
|
end
|
|
called_ae_title = called_ae_title .. string.rep(" ", 16 - #called_ae_title)
|
|
calling_ae_title = calling_ae_title .. string.rep(" ", 16 - #calling_ae_title)
|
|
|
|
-- ASSOCIATE request
|
|
local assoc_request = string.pack(">I2 I2 c16 c16 c32 c" .. application_context:len() .. " c" .. presentation_context:len() .. " c" .. userinfo_context:len(),
|
|
0x1, -- Protocol version ( 2 bytes )
|
|
0x0, -- Reserved section ( 2 bytes that should be set to 0x0 )
|
|
called_ae_title, -- Called AE title ( 16 bytes)
|
|
calling_ae_title, -- Calling AE title ( 16 bytes)
|
|
0x0, -- Reserved section ( 32 bytes set to 0x0 )
|
|
application_context,
|
|
presentation_context,
|
|
userinfo_context)
|
|
|
|
local status, header = pdu_header_encode(PDU_CODES["ASSOCIATE_REQUEST"], #assoc_request)
|
|
|
|
-- Something might be wrong with our header
|
|
if status == false then
|
|
return false, header
|
|
end
|
|
|
|
assoc_request = header .. assoc_request
|
|
|
|
stdnse.debug2("PDU len minus header:%d", #assoc_request-#header)
|
|
if #assoc_request < MIN_SIZE_ASSOC_REQ then
|
|
return false, string.format("ASSOCIATE request PDU must be at least %d bytes and we tried to send %d.", MIN_SIZE_ASSOC_REQ, #assoc_request)
|
|
end
|
|
local status, err = send(dcm, assoc_request)
|
|
if status == false then
|
|
return false, string.format("Couldn't send ASSOCIATE request:%s", err)
|
|
end
|
|
status, err = receive(dcm)
|
|
if status == false then
|
|
return false, string.format("Couldn't read ASSOCIATE response:%s", err)
|
|
end
|
|
|
|
local resp_type, _, resp_length = string.unpack(">B B I4", err)
|
|
stdnse.debug1("PDU Type:%d Length:%d", resp_type, resp_length)
|
|
if resp_type == PDU_CODES["ASSOCIATE_ACCEPT"] then
|
|
stdnse.debug1("ASSOCIATE ACCEPT message found!")
|
|
return true, dcm
|
|
elseif resp_type == PDU_CODES["ASSOCIATE_REJECT"] then
|
|
stdnse.debug1("ASSOCIATE REJECT message found!")
|
|
return false, "ASSOCIATE REJECT received"
|
|
else
|
|
return false, "Received unknown response"
|
|
end
|
|
end
|
|
|
|
function send_pdata(dicom, data)
|
|
local status, header = pdu_header_encode(PDU_CODES["DATA"], #data)
|
|
if status == false then
|
|
return false, header
|
|
end
|
|
local err
|
|
status, err = send(dicom, header .. data)
|
|
if status == false then
|
|
return false, err
|
|
end
|
|
end
|
|
|
|
return _ENV
|