description = [[ Detects the CCcam service (software for sharing subscription TV among multiple receivers). The service normally runs on port 12000. It distinguishes itself by printing 16 random-looking bytes upon receiving a connection. Because the script attempts to detect "random-looking" bytes, it has a small chance of failing to detect the service when the data do not seem random enough.]] categories = {"version"} author = "David Fifield" require("bin") require("shortport") -- A chi-square test for the null hypothesis that the members of data are drawn -- from a uniform distribution over num_cats categories. local function chi2(data, num_cats) local bins = {} local x2, delta, expected for _, x in ipairs(data) do bins[x] = bins[x] or 0 bins[x] = bins[x] + 1 end expected = #data / num_cats x2 = 0.0 for _, n in pairs(bins) do delta = n - expected x2 = x2 + delta * delta end x2 = x2 / expected return x2 end -- Split a string into a sequence of bit strings of the given length. -- splitbits("abc", 5) --> {"01100", "00101", "10001", "00110"} -- Any short final group is omitted. local function splitbits(s, n) local bits, seq _, bits = bin.unpack("B" .. #s, s) seq = {} for i = 1, #bits - n, n do seq[#seq + 1] = bits:sub(i, i + n - 1) end return seq end -- chi-square cdf table at 0.95 confidence for different degrees of freedom. -- >>> import scipy.stats, scipy.optimize -- >>> scipy.optimize.newton(lambda x: scipy.stats.chi2(dof).cdf(x) - 0.95, dof) local CHI2_CDF = { [3] = 7.8147279032511738, [15] = 24.99579013972863, [255] = 293.2478350807001, } local function looks_random(data) local x2 -- Because our sample is so small (only 16 bytes), do a chi-square -- goodness of fit test across groups of 2, 4, and 8 bits. If using only -- 8 bits, for example, any sample whose bytes are all different would -- pass the test. Using 2 bits will tend to catch things like pure -- ASCII, where one out of every four samples never has its high bit -- set. x2 = chi2(splitbits(data, 2), 4) if x2 > CHI2_CDF[3] then return false end x2 = chi2(splitbits(data, 4), 16) if x2 > CHI2_CDF[15] then return false end x2 = chi2({string.byte(data, 1, -1)}, 256) if x2 > CHI2_CDF[255] then return false end return true end local NUM_TRIALS = 2 local function trial(host, port) local status, data, s s = nmap.new_socket() status, data = s:connect(host, port) if not status then return end status, data = s:receive_bytes(0) if not status then s:close() return end s:close() return data end portrule = shortport.version_port_or_service({10000, 10001, 12000, 12001, 16000, 16001}, "cccam") function action(host, port) local seen = {} -- Try a couple of times to see that the response isn't constant. (But -- more trials also increase the chance that we will reject a legitimate -- cccam service.) for i = 1, NUM_TRIALS do local data data = trial(host, port) if not data or seen[data] or #data ~= 16 or not looks_random(data) then return end seen[data] = true end port.version.name = "cccam" port.version.version = "CCcam DVR card sharing system" nmap.set_port_version(host, port, "hardmatched") end