HEXDUMP_DLOG_MAX = HEXDUMP_DLOG_MAX or 32 NOT3=bitnot(3) NOT7=bitnot(7) -- xor pid,tid,sec,nsec math.randomseed(bitxor(getpid(),gettid(),clock_gettime())) -- basic desync function -- execute given lua code. "desync" is temporary set as global var to be accessible to the code -- useful for simple fast actions without writing a func -- arg: code= function luaexec(ctx, desync) if not desync.arg.code then error("luaexec: no 'code' parameter") end local fname = desync.func_instance.."_luaexec_code" if not _G[fname] then _G[fname] = load(desync.arg.code, fname) end -- allow dynamic code to access desync _G.desync = desync _G[fname]() _G.desync = nil end -- basic desync function -- does nothing just acknowledges when it's called -- no args function pass(ctx, desync) DLOG("pass") end -- basic desync function -- prints desync to DLOG function pktdebug(ctx, desync) DLOG("desync:") var_debug(desync) end -- basic desync function -- prints function args function argdebug(ctx, desync) var_debug(desync.arg) end -- basic desync function -- prints conntrack positions to DLOG function posdebug(ctx, desync) if not desync.track then DLOG("posdebug: no track") return end local s="posdebug: "..(desync.outgoing and "out" or "in").." time +"..desync.track.pos.dt.."s direct" for i,pos in pairs({'n','d','b','s','p'}) do s=s.." "..pos..pos_get(desync, pos, false) end s=s.." reverse" for i,pos in pairs({'n','d','b','s','p'}) do s=s.." "..pos..pos_get(desync, pos, true) end s=s.." payload "..#desync.dis.payload if desync.reasm_data then s=s.." reasm "..#desync.reasm_data end if desync.decrypt_data then s=s.." decrypt "..#desync.decrypt_data end if desync.replay_piece_count then s=s.." replay "..desync.replay_piece.."/"..desync.replay_piece_count end DLOG(s) end -- basic desync function -- set l7payload to 'arg.payload' if reasm.data or desync.dis.payload contains 'arg.pattern' substring -- NOTE : this does not set payload on C code side ! -- NOTE : C code will not see payload change. --payload args take only payloads known to C code and cause error if unknown. -- arg: pattern - substring for search inside reasm_data or desync.dis.payload -- arg: payload - set desync.l7payload to this if detected -- arg: undetected - set desync.l7payload to this if not detected -- test case : nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-init=@zapret-antidpi.lua --lua-init=@zapret-auto.lua --lua-desync=detect_payload_str:pattern=1234:payload=my --lua-desync=fake:blob=0x1234:payload=my function detect_payload_str(ctx, desync) if not desync.arg.pattern then error("detect_payload_str: missing 'pattern'") end local data = desync.reasm_data or desync.dis.payload local b = string.find(data,desync.arg.pattern,1,true) if b then DLOG("detect_payload_str: detected '"..desync.arg.payload.."'") if desync.arg.payload then desync.l7payload = desync.arg.payload end else DLOG("detect_payload_str: not detected '"..desync.arg.payload.."'") if desync.arg.undetected then desync.l7payload = desync.arg.undetected end end end -- this shim is needed then function is orchestrated. ctx services not available -- have to emulate cutoff in LUA using connection persistent table track.lua_state function instance_cutoff_shim(ctx, desync, dir) if ctx then instance_cutoff(ctx, dir) elseif not desync.track then DLOG("instance_cutoff_shim: cannot cutoff '"..desync.func_instance.."' because conntrack is absent") else if not desync.track.lua_state.cutoff_shim then desync.track.lua_state.cutoff_shim = {} end if not desync.track.lua_state.cutoff_shim[desync.func_instance] then desync.track.lua_state.cutoff_shim[desync.func_instance] = {} end if type(dir)=="nil" then -- cutoff both directions by default desync.track.lua_state.cutoff_shim[desync.func_instance][true] = true desync.track.lua_state.cutoff_shim[desync.func_instance][false] = true else desync.track.lua_state.cutoff_shim[desync.func_instance][dir] = true end if b_debug then DLOG("instance_cutoff_shim: cutoff '"..desync.func_instance.."' in="..tostring(type(dir)=="nil" and true or not dir).." out="..tostring(type(dir)=="nil" or dir)) end end end function cutoff_shim_check(desync) if not desync.track then DLOG("cutoff_shim_check: cannot check '"..desync.func_instance.."' cutoff because conntrack is absent") return false else local b=desync.track.lua_state.cutoff_shim and desync.track.lua_state.cutoff_shim[desync.func_instance] and desync.track.lua_state.cutoff_shim[desync.func_instance][desync.outgoing] if b and b_debug then DLOG("cutoff_shim_check: '"..desync.func_instance.."' "..(desync.outgoing and "out" or "in").." cutoff") end return b end end -- applies # and $ prefixes. #var means var length, %var means var value function apply_arg_prefix(desync) for a,v in pairs(desync.arg) do local c = string.sub(v,1,1) if c=='#' then local blb = blob(desync,string.sub(v,2)) desync.arg[a] = (type(blb)=='string' or type(blb)=='table') and #blb or 0 elseif c=='%' then desync.arg[a] = blob(desync,string.sub(v,2)) elseif c=='\\' then c = string.sub(v,2,2); if c=='#' or c=='%' then desync.arg[a] = string.sub(v,2) end end end end -- copy instance identification and args from execution plan to desync table -- NOTE : to not lose VERDICT_MODIFY dissect changes pass original desync table -- NOTE : if a copy was passed and VERDICT_MODIFY returned you must copy modified dissect back to desync table or resend it and return VERDICT_DROP -- NOTE : args and some fields are substituted. if you need them - make a copy before calling this. function apply_execution_plan(desync, instance) desync.func = instance.func desync.func_n = instance.func_n desync.func_instance = instance.func_instance desync.arg = deepcopy(instance.arg) apply_arg_prefix(desync) end -- produce resulting verdict from 2 verdicts function verdict_aggregate(v1, v2) local v v1 = v1 or VERDICT_PASS v2 = v2 or VERDICT_PASS if v1==VERDICT_DROP or v2==VERDICT_DROP then v=VERDICT_DROP elseif v1==VERDICT_MODIFY or v2==VERDICT_MODIFY then v=VERDICT_MODIFY else v=VERDICT_PASS end return v end function plan_instance_execute(desync, verdict, instance) apply_execution_plan(desync, instance) if cutoff_shim_check(desync) then DLOG("plan_instance_execute: not calling '"..desync.func_instance.."' because of voluntary cutoff") elseif not payload_match_filter(desync.l7payload, instance.payload_filter) then DLOG("plan_instance_execute: not calling '"..desync.func_instance.."' because payload '"..desync.l7payload.."' does not match filter '"..instance.payload_filter.."'") elseif not pos_check_range(desync, instance.range) then DLOG("plan_instance_execute: not calling '"..desync.func_instance.."' because pos "..pos_str(desync,instance.range.from).." "..pos_str(desync,instance.range.to).." is out of range '"..pos_range_str(instance.range).."'") else DLOG("plan_instance_execute: calling '"..desync.func_instance.."'") verdict = verdict_aggregate(verdict,_G[instance.func](nil, desync)) end return verdict end function plan_instance_pop(desync) return (desync.plan and #desync.plan>0) and table.remove(desync.plan, 1) end function plan_clear(desync) while table.remove(desync.plan) do end end -- this approach allows nested orchestrators function orchestrate(ctx, desync) if not desync.plan then execution_plan_cancel(ctx) desync.plan = execution_plan(ctx) end end -- copy desync preserving lua_state function desync_copy(desync) local dcopy = deepcopy(desync) if desync.track then -- preserve lua state dcopy.track.lua_state = desync.track.lua_state end if desync.plan then -- preserve execution plan dcopy.plan = desync.plan end return dcopy end -- redo what whould be done without orchestration function replay_execution_plan(desync) local verdict = VERDICT_PASS while true do local instance = plan_instance_pop(desync) if not instance then break end verdict = plan_instance_execute(desync, verdict, instance) end return verdict end -- this function demonstrates how to stop execution of upcoming desync instances and take over their job -- this can be used, for example, for orchestrating conditional processing without modifying of desync functions code -- test case : nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-desync=desync_orchestrator_example --lua-desync=pass --lua-desync=pass function desync_orchestrator_example(ctx, desync) DLOG("orchestrator: taking over upcoming desync instances") orchestrate(ctx, desync) return replay_execution_plan(desync) end -- these functions duplicate range check logic from C code -- mode must be n,d,b,s,x,a -- pos is {mode,pos} -- range is {from={mode,pos}, to={mode,pos}, upper_cutoff} -- upper_cutoff = true means non-inclusive upper boundary function pos_get_pos(track_pos, mode) if track_pos then if mode=='n' then return track_pos.pcounter elseif mode=='d' then return track_pos.pdcounter elseif mode=='b' then return track_pos.pbcounter elseif track_pos.tcp then if mode=='s' then return track_pos.tcp.rseq elseif mode=='p' then return track_pos.tcp.pos end end end return 0 end function pos_get(desync, mode, reverse) if desync.track then local track_pos = reverse and desync.track.pos.reverse or desync.track.pos.direct return pos_get_pos(track_pos,mode) end return 0 end function pos_check_from(desync, range) if range.from.mode == 'x' then return false end if range.from.mode ~= 'a' then if desync.track then return pos_get(desync, range.from.mode) >= range.from.pos else return false end end return true; end function pos_check_to(desync, range) local ps if range.to.mode == 'x' then return false end if range.to.mode ~= 'a' then if desync.track then ps = pos_get(desync, range.to.mode) return (ps < range.to.pos) or not range.upper_cutoff and (ps == range.to.pos) else return false end end return true; end function pos_check_range(desync, range) return pos_check_from(desync,range) and pos_check_to(desync,range) end function pos_range_str(range) return range.from.mode..range.from.pos..(range.upper_cutoff and '<' or '-')..range.to.mode..range.to.pos end function pos_str(desync, pos) return pos.mode..pos_get(desync, pos.mode) end function is_retransmission(desync) return desync.track and desync.track.pos.direct.tcp and 0==bitand(u32add(desync.track.pos.direct.tcp.uppos_prev, -desync.track.pos.direct.tcp.pos), 0x80000000) end -- prepare standard rawsend options from desync -- repeats - how many time send the packet -- ifout - override outbound interface (if --bind_fix4, --bind-fix6 enabled) -- fwmark - override fwmark. desync mark bit(s) will be set unconditionally function rawsend_opts(desync) return { repeats = desync.arg.repeats, ifout = desync.arg.ifout or desync.ifout, fwmark = desync.arg.fwmark or desync.fwmark } end -- only basic options. no repeats function rawsend_opts_base(desync) return { ifout = desync.arg.ifout or desync.ifout, fwmark = desync.arg.fwmark or desync.fwmark } end -- prepare standard reconstruct options from desync -- badsum - make L4 checksum invalid -- ip6_preserve_next - use next protocol fields from dissect, do not auto fill values. can be set from code only, not from args -- ip6_last_proto - last ipv6 "next" protocol. used only by "reconstruct_ip6hdr". can be set from code only, not from args function reconstruct_opts(desync) return { badsum = desync.arg.badsum } end -- combined desync opts function desync_opts(desync) return { rawsend = rawsend_opts(desync), reconstruct = reconstruct_opts(desync), ipfrag = desync.arg, ipid = desync.arg, fooling = desync.arg } end -- convert binary string to hex data function string2hex(s) local ss = "" for i = 1, #s do if i>1 then ss = ss .. " " end ss = ss .. string.format("%02X", string.byte(s, i)) end return ss end function has_nonprintable(s) return s:match("[^ -\\r\\n\\t]") end function make_readable(v) if type(v)=="string" then return string.gsub(v,"[^ -]","."); else return tostring(v) end end -- return hex dump of a binary string if it has nonprintable characters or string itself otherwise function str_or_hex(s) if has_nonprintable(s) then return string2hex(s) else return s end end function logical_xor(a,b) return a and not b or not a and b end -- print to DLOG any variable. tables are expanded in the tree form, unprintables strings are hex dumped function var_debug(v) local function dbg(v,level) if type(v)=="table" then for key, value in pairs(v) do DLOG(string.rep(" ",2*level).."."..tostring(key)) dbg(v[key],level+1) end elseif type(v)=="string" then DLOG(string.rep(" ",2*level)..type(v).." "..str_or_hex(v)) else DLOG(string.rep(" ",2*level)..type(v).." "..make_readable(v)) end end dbg(v,0) end -- make hex dump function hexdump(s,max) local l = max<#s and max or #s local ss = string.sub(s,1,l) return string2hex(ss)..(#s>max and " ... " or " " )..make_readable(ss)..(#s>max and " ... " or "" ) end -- make hex dump limited by HEXDUMP_DLOG_MAX chars function hexdump_dlog(s) return hexdump(s,HEXDUMP_DLOG_MAX) end -- make copy of an array recursively function deepcopy(orig, copies) copies = copies or {} local orig_type = type(orig) local copy if orig_type == 'table' then if copies[orig] then copy = copies[orig] else copy = {} copies[orig] = copy for orig_key, orig_value in next, orig, nil do copy[deepcopy(orig_key, copies)] = deepcopy(orig_value, copies) end setmetatable(copy, deepcopy(getmetatable(orig), copies)) end else -- number, string, boolean, etc copy = orig end return copy end -- check if string 'v' in comma separated list 's' function in_list(s, v) if s then for elem in string.gmatch(s, "[^,]+") do if elem==v then return true end end end return false end -- blobs can be 0xHEX, field name in desync or global var -- if name is nil - return def function blob(desync, name, def) if not name or #name==0 then if def then return def else error("empty blob name") end end local blob if string.sub(name,1,2)=="0x" then blob = parse_hex(string.sub(name,3)) if not blob then error("invalid hex string : "..name) end else blob = desync[name] if not blob then -- use global var if no field in dissect table blob = _G[name] if not blob then error("blob '"..name.."' unavailable") end end end return blob end function blob_or_def(desync, name, def) return name and blob(desync,name,def) or def end -- repeat pattern as needed to extract part of it with any length -- pat="12345" len=10 offset=4 => "4512345123" function pattern(pat, offset, len) if not pat or #pat==0 then error("pattern: bad or empty pattern") end local off = (offset-1) % #pat local pats = divint((len + #pat - 1), #pat) + (off==0 and 0 or 1) return string.sub(string.rep(pat,pats),off+1,off+len) end -- decrease by 1 all number values in the array function zero_based_pos(a) if not a then return nil end local b={} for i,v in ipairs(a) do b[i] = type(a[i])=="number" and a[i] - 1 or a[i] end return b end -- delete elements with number value 1 function delete_pos_1(a) local i=1 while i<=#a do if type(a[i])=="number" and a[i] == 1 then table.remove(a,i) else i = i+1 end end return a end -- find pos of the next eol and pos of the next non-eol character after eol function find_next_line(s, pos) local p1, p2 p1 = string.find(s,"[\r\n]",pos) if p1 then p2 = p1 p1 = p1-1 if string.sub(s,p2,p2)=='\r' then p2=p2+1 end if string.sub(s,p2,p2)=='\n' then p2=p2+1 end if p2>#s then p2=nil end else p1 = #s end return p1,p2 end function http_dissect_header(header) local p1,p2 p1,p2 = string.find(header,":") if p1 then p2=string.find(header,"[^ \t]",p2+1) return string.sub(header,1,p1-1), p2 and string.sub(header,p2) or "", p1-1, p2 or #header end return nil end -- make table with structured http header representation function http_dissect_headers(http, pos) local eol,pnext,header,value,idx,headers,pos_endheader,pos_startvalue headers={} while pos do eol,pnext = find_next_line(http,pos) header = string.sub(http,pos,eol) if #header == 0 then break end header,value,pos_endheader,pos_startvalue = http_dissect_header(header) if header then headers[string.lower(header)] = { header = header, value = value, pos_start = pos, pos_end = eol, pos_header_end = pos+pos_endheader-1, pos_value_start = pos+pos_startvalue-1 } end pos=pnext end return headers end -- make table with structured http request representation function http_dissect_req(http) if not http then return nil; end local eol,pnext,req,hdrpos local pos=1 -- skip methodeol empty line(s) while pos do eol,pnext = find_next_line(http,pos) req = string.sub(http,pos,eol) pos=pnext if #req>0 then break end end hdrpos = pos if not req or #req==0 then return nil end pos = string.find(req,"[ \t]") if not pos then return nil end local method = string.sub(req,1,pos-1); pos = string.find(req,"[^ \t]",pos+1) if not pos then return nil end pnext = string.find(req,"[ \t]",pos+1) if not pnext then pnext = #http + 1 end local uri = string.sub(req,pos,pnext-1) return { method = method, uri = uri, headers = http_dissect_headers(http,hdrpos) } end function http_dissect_reply(http) if not http then return nil; end local s, pos, code s = string.sub(http,1,8) if s~="HTTP/1.1" and s~="HTTP/1.0" then return nil end pos = string.find(http,"[ \t\r\n]",10) code = tonumber(string.sub(http,10,pos-1)) if not code then return nil end pos = find_next_line(http,pos) return { code = code, headers = http_dissect_headers(http,pos) } end function dissect_url(url) local p1,pb,pstart,pend local proto, creds, domain, port, uri p1 = string.find(url,"[^ \t]") if not p1 then return nil end pb = p1 pstart,pend = string.find(url,"[a-z]+://",p1) if pend then proto = string.sub(url,pstart,pend-3) p1 = pend+1 end pstart,pend = string.find(url,"[@/]",p1) if pend and string.sub(url,pstart,pend)=='@' then creds = string.sub(url,p1,pend-1) p1 = pend+1 end pstart,pend = string.find(url,"/",p1,true) if pend then if pend==pb then uri = string.sub(url,pb) else uri = string.sub(url,pend) domain = string.sub(url,p1,pend-1) end else if proto then domain = string.sub(url,p1) else uri = string.sub(url,p1) end end if domain then pstart,pend = string.find(domain,':',1,true) if pend then port = string.sub(domain, pend+1) domain = string.sub(domain, 1, pstart-1) end end return { proto = proto, creds = creds, domain = domain, port = port, uri=uri } end function dissect_nld(domain, level) if domain then local n=1 for pos=#domain,1,-1 do if string.sub(domain,pos,pos)=='.' then if n==level then return string.sub(domain, pos+1) end n=n+1 end end if n==level then return domain end end return nil end -- support sni=%var function tls_mod_shim(desync, blob, modlist, payload) local p1,p2 = string.find(modlist,"sni=%%[^,]+") if p1 then local var = string.sub(modlist,p1+5,p2) local val = desync[var] or _G[var] if not val then error("tls_mod_shim: non-existent var '"..var.."'") end modlist = string.sub(modlist,1,p1+3)..val..string.sub(modlist,p2+1) end return tls_mod(blob,modlist,payload) end -- convert comma separated list of tcp flags to tcp.th_flags bit field function parse_tcp_flags(s) local flags={FIN=TH_FIN, SYN=TH_SYN, RST=TH_RST, PSH=TH_PUSH, PUSH=TH_PUSH, ACK=TH_ACK, URG=TH_URG, ECE=TH_ECE, CWR=TH_CWR} local f=0 local s_upper = string.upper(s) for flag in string.gmatch(s_upper, "[^,]+") do if flags[flag] then f = bitor(f,flags[flag]) else error("tcp flag '"..flag.."' is invalid") end end return f end -- find first tcp options of specified kind in dissect.tcp.options function find_tcp_option(options, kind) if options then for i, opt in pairs(options) do if opt.kind==kind then return i end end end return nil end -- find first ipv6 extension header of specified protocol in dissect.ip6.exthdr function find_ip6_exthdr(exthdr, proto) if exthdr then for i, hdr in pairs(exthdr) do if hdr.type==proto then return i end end end return nil end -- insert ipv6 extension header at specified index. fix next proto chain function insert_ip6_exthdr(ip6, idx, header_type, data) local prev if not ip6.exthdr then ip6.exthdr={} end if not idx then -- insert to the end idx = #ip6.exthdr+1 elseif idx<0 or idx>(#ip6.exthdr+1) then error("insert_ip6_exthdr: invalid index "..idx) end if idx==1 then prev = ip6.ip6_nxt ip6.ip6_nxt = header_type else prev = ip6.exthdr[idx-1].next ip6.exthdr[idx-1].next = header_type end table.insert(ip6.exthdr, idx, {type = header_type, data = data, next = prev}) end -- delete ipv6 extension header at specified index. fix next proto chain function del_ip6_exthdr(ip6, idx) if idx<=0 or idx>#ip6.exthdr then error("delete_ip6_exthdr: nonexistent index "..idx) end local nxt = ip6.exthdr[idx].next if idx==1 then ip6.ip6_nxt = nxt else ip6.exthdr[idx-1].next = nxt end table.remove(ip6.exthdr, idx) end -- fills next proto fields in ipv6 header and extension headers function fix_ip6_next(ip6, last_proto) if ip6.exthdr and #ip6.exthdr>0 then for i=1,#ip6.exthdr do if i==1 then -- first header ip6.ip6_nxt = ip6.exthdr[i].type end ip6.exthdr[i].next = i==#ip6.exthdr and (last_proto or IPPROTO_NONE) or ip6.exthdr[i+1].type end else -- no headers ip6.ip6_nxt = last_proto or IPPROTO_NONE end end -- parse autottl : delta,min-max function parse_autottl(s) if s then local delta,min,max = string.match(s,"([-+]?%d+),(%d+)-(%d+)") min = tonumber(min) max = tonumber(max) delta = tonumber(delta) if not delta or min>max then error("parse_autottl: invalid value '"..s.."'") end return {delta=delta,min=min,max=max} else return nil end end -- calculate ttl value based on incoming_ttl and parsed attl definition (delta,min-max) function autottl(incoming_ttl, attl) local function hop_count_guess(incoming_ttl) -- 18.65.168.125 ( cloudfront ) 255 -- 157.254.246.178 128 -- 1.1.1.1 64 -- guess original ttl. consider path lengths less than 32 hops local orig if incoming_ttl>223 then orig=255 elseif incoming_ttl<128 and incoming_ttl>96 then orig=128 elseif incoming_ttl<64 and incoming_ttl>32 then orig=64 else return nil end return orig-incoming_ttl end -- return guessed fake ttl value. 0 means unsuccessfull, should not perform autottl fooling local function autottl_eval(hop_count, attl) local d,fake d = hop_count + attl.delta if dattl.max then fake=attl.max else fake=d end if attl.delta<0 and fake>=hop_count or attl.delta>=0 and fake - set tcp flags in comma separated list -- tcp_flags_unset= - unset tcp flags in comma separated list -- tcp_ts_up - move timestamp tcp option to the top if it's present. this allows linux not to accept badack segments without badseq. this is very strange discovery but it works. -- fool - custom fooling function : fool_func(dis, fooling_options) function apply_fooling(desync, dis, fooling_options) local function prepare_bin(hex,def) local bin = parse_hex(hex) if not bin then error("apply_fooling: invalid hex string '"..hex.."'") end return #bin>0 and bin or def end local function ttl_discover(arg_ttl,arg_autottl) local ttl if arg_autottl and desync.track then if desync.track.incoming_ttl then -- use lua_cache to store discovered autottl if type(desync.track.lua_state.autottl_cache)~="table" then desync.track.lua_state.autottl_cache={} end if type(desync.track.lua_state.autottl_cache[desync.func_instance])~="table" then desync.track.lua_state.autottl_cache[desync.func_instance]={} end if not desync.track.lua_state.autottl_cache[desync.func_instance].autottl_found then desync.track.lua_state.autottl_cache[desync.func_instance].autottl = autottl(desync.track.incoming_ttl,parse_autottl(arg_autottl)) if desync.track.lua_state.autottl_cache[desync.func_instance].autottl then desync.track.lua_state.autottl_cache[desync.func_instance].autottl_found = true DLOG("apply_fooling: discovered autottl "..desync.track.lua_state.autottl_cache[desync.func_instance].autottl) else DLOG("apply_fooling: could not discover autottl") end elseif desync.track.lua_state.autottl_cache[desync.func_instance].autottl then DLOG("apply_fooling: using cached autottl "..desync.track.lua_state.autottl_cache[desync.func_instance].autottl) end ttl=desync.track.lua_state.autottl_cache[desync.func_instance].autottl else DLOG("apply_fooling: cannot apply autottl because incoming ttl unknown") end end if not ttl and tonumber(arg_ttl) then ttl = tonumber(arg_ttl) end --io.stderr:write("TTL "..tostring(ttl).."\n") return ttl end local function move_ts_top() local tsidx = find_tcp_option(dis.tcp.options, TCP_KIND_TS) if tsidx and tsidx>1 then table.insert(dis.tcp.options, 1, dis.tcp.options[tsidx]) table.remove(dis.tcp.options, tsidx + 1) end end -- take default fooling from desync.arg if not fooling_options then fooling_options = desync.arg end -- use current packet if dissect not given if not dis then dis = desync.dis end if dis.tcp then if tonumber(fooling_options.tcp_seq) then dis.tcp.th_seq = u32add(dis.tcp.th_seq, fooling_options.tcp_seq) end if tonumber(fooling_options.tcp_ack) then dis.tcp.th_ack = u32add(dis.tcp.th_ack, fooling_options.tcp_ack) end if fooling_options.tcp_flags_unset then dis.tcp.th_flags = bitand(dis.tcp.th_flags, bitnot(parse_tcp_flags(fooling_options.tcp_flags_unset))) end if fooling_options.tcp_flags_set then dis.tcp.th_flags = bitor(dis.tcp.th_flags, parse_tcp_flags(fooling_options.tcp_flags_set)) end if tonumber(fooling_options.tcp_ts) then local idx = find_tcp_option(dis.tcp.options,TCP_KIND_TS) if idx and (dis.tcp.options[idx].data and #dis.tcp.options[idx].data or 0)==8 then dis.tcp.options[idx].data = bu32(u32add(u32(dis.tcp.options[idx].data),fooling_options.tcp_ts))..string.sub(dis.tcp.options[idx].data,5) else DLOG("apply_fooling: timestamp tcp option not present or invalid") end end if fooling_options.tcp_md5 then if find_tcp_option(dis.tcp.options,TCP_KIND_MD5) then DLOG("apply_fooling: md5 option already present") else table.insert(dis.tcp.options,{kind=TCP_KIND_MD5, data=prepare_bin(fooling_options.tcp_md5,brandom(16))}) end end if fooling_options.tcp_ts_up then move_ts_top(dis.tcp.options) end end if dis.ip6 then local bin if fooling_options.ip6_hopbyhop then bin = prepare_bin(fooling_options.ip6_hopbyhop,"\x00\x00\x00\x00\x00\x00") insert_ip6_exthdr(dis.ip6,nil,IPPROTO_HOPOPTS,bin) end if fooling_options.ip6_hopbyhop2 then bin = prepare_bin(fooling_options.ip6_hopbyhop2,"\x00\x00\x00\x00\x00\x00") insert_ip6_exthdr(dis.ip6,nil,IPPROTO_HOPOPTS,bin) end -- for possible unfragmentable part if fooling_options.ip6_destopt then bin = prepare_bin(fooling_options.ip6_destopt,"\x00\x00\x00\x00\x00\x00") insert_ip6_exthdr(dis.ip6,nil,IPPROTO_DSTOPTS,bin) end if fooling_options.ip6_routing then bin = prepare_bin(fooling_options.ip6_routing,"\x00\x00\x00\x00\x00\x00") insert_ip6_exthdr(dis.ip6,nil,IPPROTO_ROUTING,bin) end -- for possible fragmentable part if fooling_options.ip6_destopt2 then bin = prepare_bin(fooling_options.ip6_destopt2,"\x00\x00\x00\x00\x00\x00") insert_ip6_exthdr(dis.ip6,nil,IPPROTO_DSTOPTS,bin) end if fooling_options.ip6_ah then -- by default truncated authentication header - only 6 bytes bin = prepare_bin(fooling_options.ip6_ah,"\x00\x00"..brandom(4)) insert_ip6_exthdr(dis.ip6,nil,IPPROTO_AH,bin) end end if dis.ip then local ttl = ttl_discover(fooling_options.ip_ttl,fooling_options.ip_autottl) if ttl then dis.ip.ip_ttl = ttl end end if dis.ip6 then local ttl = ttl_discover(fooling_options.ip6_ttl,fooling_options.ip6_autottl) if ttl then dis.ip6.ip6_hlim = ttl end end if fooling_options.fool and #fooling_options.fool>0 then if type(_G[fooling_options.fool])=="function" then DLOG("apply_fooling: calling '"..fooling_options.fool.."'") _G[fooling_options.fool](dis, fooling_options) else error("apply_fooling: fool function '"..tostring(fooling_options.fool).."' does not exist") end end end -- assign dis.ip.ip_id value according to policy in ipid_options or desync.arg. apply def or "seq" policy if no ip_id options -- ip_id=seq|rnd|zero|none -- ip_id_conn - in 'seq' mode save current ip_id in track.lua_state to use it between packets -- remember ip_id in desync function apply_ip_id(desync, dis, ipid_options, def) -- use current packet if dissect not given if not dis then dis = desync.dis end if dis.ip then -- ip_id is ipv4 only, ipv6 doesn't have it -- take default ipid options from desync.arg if not ipid_options then ipid_options = desync.arg end local mode = ipid_options.ip_id or def or "seq" if mode == "seq" then if desync.track and ipid_options.ip_id_conn then dis.ip.ip_id = desync.track.lua_state.ip_id or dis.ip.ip_id desync.track.lua_state.ip_id = dis.ip.ip_id + 1 else dis.ip.ip_id = desync.ip_id or dis.ip.ip_id desync.ip_id = dis.ip.ip_id + 1 end elseif mode == "zero" then dis.ip.ip_id = 0 elseif mode == "rnd" then dis.ip.ip_id = math.random(1,0xFFFF) end end end -- return length of ipv4 or ipv6 header without options and extension headers. should be 20 for ipv4 and 40 for ipv6. function l3_base_len(dis) if dis.ip then return IP_BASE_LEN elseif dis.ip6 then return IP6_BASE_LEN else return 0 end end -- return length of ipv4 options or summary length of all ipv6 extension headers -- ip6_exthdr_last_idx - count lengths for headers up to this index function l3_extra_len(dis, ip6_exthdr_last_idx) local l=0 if dis.ip then if dis.ip.options then l = bitand(#dis.ip.options+3,NOT3) end elseif dis.ip6 and dis.ip6.exthdr then local ct if ip6_exthdr_last_idx and ip6_exthdr_last_idx<=#dis.ip6.exthdr then ct = ip6_exthdr_last_idx else ct = #dis.ip6.exthdr end for i=1, ct do if dis.ip6.exthdr[i].type == IPPROTO_AH then -- length in 32-bit words l = l + bitand(3+2+#dis.ip6.exthdr[i].data,NOT3) else -- length in 64-bit words l = l + bitand(7+2+#dis.ip6.exthdr[i].data,NOT7) end end end return l end -- return length of ipv4/ipv6 header with options/extension headers function l3_len(dis) return l3_base_len(dis)+l3_extra_len(dis) end -- return length of tcp/udp headers without options. should be 20 for tcp and 8 for udp. function l4_base_len(dis) if dis.tcp then return TCP_BASE_LEN elseif dis.udp then return UDP_BASE_LEN else return 0 end end -- return length of tcp options or 0 if not tcp function l4_extra_len(dis) local l=0 if dis.tcp and dis.tcp.options then for i=1, #dis.tcp.options do l = l + 1 if dis.tcp.options[i].kind~=TCP_KIND_NOOP and dis.tcp.options[i].kind~=TCP_KIND_END then l = l + 1 if dis.tcp.options[i].data then l = l + #dis.tcp.options[i].data end end end -- 4 byte aligned l = bitand(3+l,NOT3) end return l end -- return length of tcp header with options or base length of udp header - 8 bytes function l4_len(dis) return l4_base_len(dis)+l4_extra_len(dis) end -- return summary extra length of ipv4/ipv6 and tcp headers. 0 if no options, no ext headers function l3l4_extra_len(dis) return l3_extra_len(dis)+l4_extra_len(dis) end -- return summary length of ipv4/ipv6 and tcp/udp headers function l3l4_len(dis) return l3_len(dis)+l4_len(dis) end -- return summary length of ipv4/ipv6 , tcp/udp headers and payload function packet_len(dis) return l3l4_len(dis) + #dis.payload end -- option : ipfrag.ipfrag_disorder - send fragments from last to first function rawsend_dissect_ipfrag(dis, options) if options and options.ipfrag and options.ipfrag.ipfrag then local frag_func = options.ipfrag.ipfrag=="" and "ipfrag2" or options.ipfrag.ipfrag if type(_G[frag_func]) ~= "function" then error("rawsend_dissect_ipfrag: ipfrag function '"..tostring(frag_func).."' does not exist") end local fragments = _G[frag_func](dis, options.ipfrag) -- allow ipfrag function to do extheader magic with non-standard "next protocol" -- NOTE : dis.ip6 must have valid next protocol fields !!!!! local reconstruct_frag = options.reconstruct and deepcopy(options.reconstruct) or {} reconstruct_frag.ip6_preserve_next = true if fragments then if options.ipfrag.ipfrag_disorder then for i=#fragments,1,-1 do DLOG("sending ip fragment "..i) -- C function if not rawsend_dissect(fragments[i], options.rawsend, reconstruct_frag) then return false end end else for i, d in pairs(fragments) do DLOG("sending ip fragment "..i) -- C function if not rawsend_dissect(d, options.rawsend, reconstruct_frag) then return false end end end return true end -- ipfrag failed. send unfragmented end -- C function return rawsend_dissect(dis, options and options.rawsend, options and options.reconstruct) end -- send dissect with tcp segmentation based on mss value. appply specified rawsend options. function rawsend_dissect_segmented(desync, dis, mss, options) local discopy = deepcopy(dis) apply_fooling(desync, discopy, options and options.fooling) if dis.tcp then local extra_len = l3l4_extra_len(discopy) if extra_len >= mss then return false end local max_data = mss - extra_len if #discopy.payload > max_data then local pos=1 local len local payload=discopy.payload while pos <= #payload do len = #payload - pos + 1 if len > max_data then len = max_data end discopy.payload = string.sub(payload,pos,pos+len-1) apply_ip_id(desync, discopy, options and options.ipid) if not rawsend_dissect_ipfrag(discopy, options) then -- stop if failed return false end discopy.tcp.th_seq = discopy.tcp.th_seq + len pos = pos + len end return true end end apply_ip_id(desync, discopy, options and options.ipid) -- no reason to segment return rawsend_dissect_ipfrag(discopy, options) end -- send specified payload based on existing L3/L4 headers in the dissect. add seq to tcp.th_seq. function rawsend_payload_segmented(desync, payload, seq, options) options = options or desync_opts(desync) local dis = deepcopy(desync.dis) if payload then dis.payload = payload end if dis.tcp and seq then dis.tcp.th_seq = dis.tcp.th_seq + seq end return rawsend_dissect_segmented(desync, dis, desync.tcp_mss, options) end -- check if desync.outgoing comply with arg.dir or def if it's not present or "out" of they are not present both. dir can be "in","out","any" function direction_check(desync, def) local dir = desync.arg.dir or def or "out" return desync.outgoing and desync.arg.dir~="in" or not desync.outgoing and dir~="out" end -- if dir "in" or "out" cutoff current desync function from opposite direction function direction_cutoff_opposite(ctx, desync, def) local dir = desync.arg.dir or def or "out" if dir=="out" then -- cutoff in instance_cutoff_shim(ctx, desync, false) elseif dir=="in" then -- cutoff out instance_cutoff_shim(ctx, desync, true) end end -- return true if l7payload matches filter l7payload_filter - comma separated list of payload types function payload_match_filter(l7payload, l7payload_filter, def) local argpl = l7payload_filter or def or "known" local neg = string.sub(argpl,1,1)=="~" local pl = neg and string.sub(argpl,2) or argpl return neg ~= (in_list(pl, "all") or in_list(pl, l7payload) or in_list(pl, "known") and l7payload~="unknown" and l7payload~="empty") end -- check if desync payload type comply with payload type list in arg.payload -- if arg.payload is not present - check for known payload - not empty and not unknown (nfqws1 behavior without "--desync-any-protocol" option) -- if arg.payload is prefixed with '~' - it means negation function payload_check(desync, def) local b = payload_match_filter(desync.l7payload, desync.arg.payload, def) if not b and b_debug then local argpl = desync.arg.payload or def or "known" DLOG("payload_check: payload '"..desync.l7payload.."' does not pass '"..argpl.."' filter") end return b end -- return name of replay drop field in track.lua_state for the current desync function instance function replay_drop_key(desync) return desync.func_instance .. "_replay_drop" end -- set/unset replay drop flag in track.lua_state for the current desync function instance function replay_drop_set(desync, v) if desync.track then if v == nil then v=true end local rdk = replay_drop_key(desync) if v then if desync.replay then desync.track.lua_state[replay_drop_key] = true end else desync.track.lua_state[replay_drop_key] = nil end end end -- auto unset replay drop flag if desync is not replay or it's the last replay piece -- return true if the caller should return VERDICT_DROP function replay_drop(desync) if desync.track then local drop = desync.replay and desync.track.lua_state[replay_drop_key] if not desync.replay or desync.replay_piece_last then -- replay stopped or last piece of reasm replay_drop_set(desync, false) end if drop then DLOG("dropping replay packet because reasm was already sent") return true end end return false end -- true if desync is not replay or it's the first replay piece function replay_first(desync) return not desync.replay or desync.replay_piece==1 end -- generate random host -- template "google.com", len=16 : h82aj.google.com -- template "google.com", len=11 : .google.com -- template "google.com", len=10 : google.com -- template "google.com", len=7 : gle.com -- no template, len=6 : b8c54a -- no template, len=7 : u9a.edu -- no template, len=10 : jgha7c.com function genhost(len, template) if template and #template>0 then if len <= #template then return string.sub(template,#template-len+1) elseif len==(#template+1) then return "."..template else return brandom_az(1)..brandom_az09(len-#template-2).."."..template end else if len>=7 then local tlds = {"com","org","net","edu","gov","biz"} local tld = tlds[math.random(#tlds)] return brandom_az(1)..brandom_az09(len-#tld-1-1).."."..tld else return brandom_az(1)..brandom_az09(len-1) end end end -- return ip addr of target host in text form function host_ip(desync) return desync.target.ip and ntop(desync.target.ip) or desync.target.ip6 and ntop(desync.target.ip6) end -- return hostname of target host if present or ip address in text form otherwise function host_or_ip(desync) if desync.track and desync.track.hostname then return desync.track.hostname end return host_ip(desync) end function is_absolute_path(path) if string.sub(path,1,1)=='/' then return true end local un = uname() return string.sub(un.sysname,1,6)=="CYGWIN" and string.sub(path,2,2)==':' end function append_path(path,file) return string.sub(path,#path,#path)=='/' and path..file or path.."/"..file end function writeable_file_name(filename) if is_absolute_path(filename) then return filename end local writedir = os.getenv("WRITEABLE") if not writedir then return filename end return append_path(writedir, filename) end -- arg : wsize=N . tcp window size -- arg : scale=N . tcp option scale factor -- return : true of changed anything function wsize_rewrite(dis, arg) local b = false if arg.wsize then local wsize = tonumber(arg.wsize) DLOG("window size "..dis.tcp.th_win.." => "..wsize) dis.tcp.th_win = tonumber(arg.wsize) b = true end if arg.scale then local scale = tonumber(arg.scale) local i = find_tcp_option(dis.tcp.options, TCP_KIND_SCALE) if i then local oldscale = u8(dis.tcp.options[i].data) if scale>oldscale then DLOG("not increasing scale factor") elseif scale "..scale) dis.tcp.options[i].data = bu8(scale) b = true end end end return b end -- standard fragmentation to 2 ip fragments -- function returns 2 dissects with fragments -- option : ipfrag_pos_udp - udp frag position. ipv4 : starting from L4 header. ipb6: starting from fragmentable part. must be multiple of 8. default 8 -- option : ipfrag_pos_tcp - tcp frag position. ipv4 : starting from L4 header. ipb6: starting from fragmentable part. must be multiple of 8. default 32 -- option : ipfrag_next - next protocol field in ipv6 fragment extenstion header of the second fragment. same as first by default. function ipfrag2(dis, ipfrag_options) local function frag_idx(exthdr) -- fragment header after hopbyhop, destopt, routing -- allow second destopt header to be in fragmentable part -- test case : --lua-desync=send:ipfrag:ipfrag_pos_tcp=40:ip6_hopbyhop:ip6_destopt:ip6_destopt2 -- WINDOWS may not send second ipv6 fragment with next protocol 60 (destopt) -- test case windows : --lua-desync=send:ipfrag:ipfrag_pos_tcp=40:ip6_hopbyhop:ip6_destopt:ip6_destopt2:ipfrag_next=255 if exthdr then local first_destopts for i=1,#exthdr do if exthdr[i].type==IPPROTO_DSTOPTS then first_destopts = i break end end for i=#exthdr,1,-1 do if exthdr[i].type==IPPROTO_HOPOPTS or exthdr[i].type==IPPROTO_ROUTING or (exthdr[i].type==IPPROTO_DSTOPTS and i==first_destopts) then return i+1 end end end return 1 end local pos local dis1, dis2 local l3 if dis.tcp then pos = ipfrag_options.ipfrag_pos_tcp or 32 elseif dis.udp then pos = ipfrag_options.ipfrag_pos_udp or 8 else pos = ipfrag_options.ipfrag_pos or 32 end DLOG("ipfrag2") if not pos then error("ipfrag2: no frag position") end l3 = l3_len(dis) if bitand(pos,7)~=0 then error("ipfrag2: frag position must be multiple of 8") end if (pos+l3)>0xFFFF then error("ipfrag2: too high frag offset") end local plen = l3 + l4_len(dis) + #dis.payload if (pos+l3)>=plen then DLOG("ipfrag2: ip frag pos exceeds packet length. ipfrag cancelled.") return nil end if dis.ip then -- ipv4 frag is done by both lua and C part -- lua code must correctly set ip_len, IP_MF and ip_off and provide full unfragmented payload -- ip_len must be set to valid value as it would appear in the fragmented packet -- ip_off must be set to fragment offset and IP_MF bit must be set if it's not the last fragment -- C code constructs unfragmented packet then moves everything after ip header according to ip_off and ip_len -- ip_id must not be zero or fragment will be dropped local ip_id = dis.ip.ip_id==0 and math.random(1,0xFFFF) or dis.ip.ip_id dis1 = deepcopy(dis) -- ip_len holds the whole packet length starting from the ip header. it includes ip, transport headers and payload dis1.ip.ip_len = l3 + pos -- ip header + first part up to frag pos dis1.ip.ip_off = IP_MF -- offset 0, IP_MF - more fragments dis1.ip.ip_id = ip_id dis2 = deepcopy(dis) dis2.ip.ip_off = bitrshift(pos,3) -- offset = frag pos, IP_MF - not set dis2.ip.ip_len = plen - pos -- unfragmented packet length - frag pos dis2.ip.ip_id = ip_id end if dis.ip6 then -- ipv6 frag is done by both lua and C part -- lua code must insert fragmentation extension header at any desirable position, fill fragment offset, more fragments flag and ident -- lua must set up ip6_plen as it would appear in the fragmented packet -- C code constructs unfragmented packet then moves fragmentable part as needed local idxfrag = frag_idx(dis.ip6.exthdr) local l3extra = l3_extra_len(dis, idxfrag-1) + 8 -- all ext headers before frag + 8 bytes for frag header local ident = math.random(1,0xFFFFFFFF) dis1 = deepcopy(dis) insert_ip6_exthdr(dis1.ip6, idxfrag, IPPROTO_FRAGMENT, bu16(IP6F_MORE_FRAG)..bu32(ident)) dis1.ip6.ip6_plen = l3extra + pos dis2 = deepcopy(dis) insert_ip6_exthdr(dis2.ip6, idxfrag, IPPROTO_FRAGMENT, bu16(pos)..bu32(ident)) -- only next proto of the first fragment is considered by standard -- fragments with non-zero offset can have different "next protocol" field -- this can be used to evade protection systems if ipfrag_options.ipfrag_next then dis2.ip6.exthdr[idxfrag].next = tonumber(ipfrag_options.ipfrag_next) end dis2.ip6.ip6_plen = plen - IP6_BASE_LEN + 8 - pos -- packet len without frag + 8 byte frag header - ipv6 base header end return {dis1,dis2} end