Skip to content

Commit

Permalink
feature: Xagent protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
buptczq committed Feb 25, 2021
1 parent 72c0d83 commit 04175e5
Show file tree
Hide file tree
Showing 7 changed files with 329 additions and 4 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ Benefit by Windows Certificate Management, this project natively supports the us

## Compatibility

There are many different OpenSSH agent implementations in Windows. This project implements 4 popular protocols in Windows:
There are many different OpenSSH agent implementations in Windows. This project implements five popular protocols in Windows:

* Cygwin UNIX Socket
* Windows UNIX Socket (Windows 10 1803 or later)
* Named pipe
* Pageant SSH Agent Protocol
* XShell Xagent Protocol

With the support of these protocols, this project is compatible with most SSH clients in Windows. For example:

Expand All @@ -35,7 +36,7 @@ With the support of these protocols, this project is compatible with most SSH cl
* Windows OpenSSH
* Putty
* Jetbrains
* SecureCRT 8.X
* SecureCRT
* XShell
* Cygwin
* MINGW
Expand Down
3 changes: 3 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
APP_HYPERV
APP_SECURECRT
APP_PAGEANT
APP_XSHELL
APP_PUBKEY
APP_WSL2
MENU_QUIT
Expand All @@ -34,6 +35,7 @@ var appIdToName = map[AppId]string{
APP_WINSSH: "WinSSH",
APP_SECURECRT: "SecureCRT",
APP_PAGEANT: "Pageant",
APP_XSHELL: "XShell",
APP_HYPERV: "Hyper-V",
}

Expand All @@ -43,6 +45,7 @@ var appIdToFullName = map[AppId]string{
APP_WINSSH: "Windows OpenSSH",
APP_SECURECRT: "SecureCRT",
APP_PAGEANT: "Pageant",
APP_XSHELL: "XShell",
APP_HYPERV: "Hyper-V",
}

Expand Down
176 changes: 176 additions & 0 deletions app/xshell.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package app

import (
"context"
"encoding/binary"
"fmt"
"github.com/buptczq/WinCryptSSHAgent/utils"
"golang.org/x/crypto/ssh"
"io"
"net"
"sync"
)

const (
maxAgentResponseBytes = 16 << 20
agentSignRequest = 13
)

type XShell struct {
cookie string
}

func (s *XShell) Run(ctx context.Context, handler func(conn io.ReadWriteCloser)) error {
s.cookie = utils.RandomString(7)
win, err := utils.NewXAgent(s.cookie)
if err != nil {
return err
}
defer win.Close()

wg := new(sync.WaitGroup)
l := win.Listener()
for {
conn, err := l.Accept()
if err != nil {
if err != io.ErrClosedPipe {
return err
}
return nil
}
err = xshellHandshake(conn, s.cookie)
if err != nil {
println(err.Error())
conn.Close()
continue
}
wg.Add(1)
go func(c io.ReadWriteCloser) {
w := &xshellProxy{c, nil}
handler(w)
wg.Done()
}(conn)
}
}

func (*XShell) AppId() AppId {
return APP_XSHELL
}

func (s *XShell) Menu(register func(id AppId, name string, handler func())) {
}

type initAgentMsg struct {
Flag uint32 `sshtype:"99"`
Length uint32
Cookie []byte `ssh:"rest"`
}

type initAgentRepMsg struct {
Flag uint32 `sshtype:"99"`
}

func xshellHandshake(conn net.Conn, cookie string) error {
var length [4]byte
if _, err := io.ReadFull(conn, length[:]); err != nil {
return err
}
l := binary.BigEndian.Uint32(length[:]) + 4
if l > maxAgentResponseBytes {
return fmt.Errorf("xagent: request too large: %d", l)
}

req := make([]byte, l)
if _, err := io.ReadFull(conn, req); err != nil {
return err
}
if req[0] != 99 {
return fmt.Errorf("xagent: unknown opcode: %d", req[0])
}

var initMsg initAgentMsg
if err := ssh.Unmarshal(req, &initMsg); err != nil {
return err
}
if int(initMsg.Length) != len(cookie) {
return fmt.Errorf("xagent: invalid cookie length")
}
if int(initMsg.Length) < len(initMsg.Cookie) {
return fmt.Errorf("xagent: invalid message length")
}

cookieRemain := make([]byte, int(initMsg.Length)-len(initMsg.Cookie))
if _, err := io.ReadFull(conn, cookieRemain); err != nil {
return err
}
cookieReq := string(initMsg.Cookie) + string(cookieRemain)
if cookieReq != cookie {
return fmt.Errorf("xagent: invalid cookie")
}
var repMsg initAgentRepMsg
repMsg.Flag = initMsg.Flag
rep := ssh.Marshal(&repMsg)
binary.BigEndian.PutUint32(length[:], uint32(len(rep)))
if _, err := conn.Write(length[:]); err != nil {
return err
}
if _, err := conn.Write(rep); err != nil {
return err
}
return nil
}

type xshellProxy struct {
conn io.ReadWriteCloser
buf []byte
}

type signRequestAgentMsg struct {
KeyBlob []byte `sshtype:"13"`
Data []byte
Flags uint32
}

func (s *xshellProxy) Read(p []byte) (n int, err error) {
if len(s.buf) > 0 {
n := copy(p, s.buf)
s.buf = s.buf[n:]
return n, nil
}
var length [4]byte
if _, err := io.ReadFull(s.conn, length[:]); err != nil {
return 0, err
}
l := binary.BigEndian.Uint32(length[:])
if l == 0 {
return 0, io.ErrUnexpectedEOF
}
if l > maxAgentResponseBytes {
return 0, fmt.Errorf("xagent: request too large: %d", l)
}

s.buf = make([]byte, 4+l, 8+l)
if _, err := io.ReadFull(s.conn, s.buf[4:]); err != nil {
return 0, err
}
// sign
if s.buf[4] == agentSignRequest {
var req signRequestAgentMsg
if err := ssh.Unmarshal(s.buf[4:], &req); err != nil {
l += 4
s.buf = append(s.buf, []byte{0, 0, 0, 0}...)
}
}
binary.BigEndian.PutUint32(s.buf, l)
n = copy(p, s.buf)
s.buf = s.buf[n:]
return n, nil
}

func (s *xshellProxy) Write(p []byte) (n int, err error) {
return s.conn.Write(p)
}

func (s *xshellProxy) Close() error {
return s.conn.Close()
}
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var applications = []app.Application{
new(app.Cygwin),
new(app.NamedPipe),
new(app.Pageant),
new(app.XShell),
}

var installHVService = flag.Bool("i", false, "Install Hyper-V Guest Communication Services")
Expand Down
5 changes: 4 additions & 1 deletion sshagent/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ func (s *Server) SSHAgentHandler(conn io.ReadWriteCloser) {
if s.Agent == nil {
return
}
agent.ServeAgent(s.Agent, conn)
err := agent.ServeAgent(s.Agent, conn)
if err != nil {
println(err.Error())
}
}
25 changes: 24 additions & 1 deletion utils/misc.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package utils

import "encoding/hex"
import (
cryptornd "crypto/rand"
"encoding/hex"
"math/rand"
"time"
)

func UUIDToString(uuid [16]byte) string {
var buf [35]byte
Expand All @@ -14,3 +19,21 @@ func UUIDToString(uuid [16]byte) string {
}
return string(buf[:])
}

func RandomString(n int) string {
const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
var bytes = make([]byte, n)
var randby bool
if num, err := cryptornd.Read(bytes); num != n || err != nil {
rand.Seed(time.Now().UnixNano())
randby = true
}
for i, b := range bytes {
if randby {
bytes[i] = alphanum[rand.Intn(len(alphanum))]
} else {
bytes[i] = alphanum[b%byte(len(alphanum))]
}
}
return string(bytes)
}
118 changes: 118 additions & 0 deletions utils/xagent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package utils

import (
"golang.org/x/sys/windows"
"net"
"syscall"
"unsafe"
)

const (
xAgentClassName = "NSSSH:AGENTWND"
xAgentSingleClassName = "STATIC"
xAgentSingleWindowName = "_SINGLE_INSTANCE::XAGENT"
)

var (
pSetWindowLong = u32.NewProc("SetWindowLongW")
)

type XAgent struct {
socket net.Listener
cookieWin *window
singleInstanceWin *window
}

type window struct {
class *wndClassEx
window windows.Handle
}

func createDefaultWindow(class, name string) (*window, error) {
classNamePtr, err := syscall.UTF16PtrFromString(class)
if err != nil {
return nil, err
}

windowNamePtr, err := syscall.UTF16PtrFromString(name)
if err != nil {
return nil, err
}

win := new(window)
wcex := &wndClassEx{
WndProc: pDefWindowProc.Addr(),
ClassName: classNamePtr,
}
err = wcex.register()
if err != nil {
return nil, err
}
win.class = wcex

windowHandle, _, err := pCreateWindowEx.Call(
uintptr(0),
uintptr(unsafe.Pointer(classNamePtr)),
uintptr(unsafe.Pointer(windowNamePtr)),
uintptr(0),
uintptr(0),
uintptr(0),
uintptr(0),
uintptr(0),
uintptr(0),
uintptr(0),
uintptr(0),
uintptr(0),
)
if windowHandle == 0 {
wcex.unregister()
return nil, err
}
win.window = windows.Handle(windowHandle)
return win, nil
}

func (s *window) Close() {
pDestroyWindow.Call(uintptr(s.window))
s.class.unregister()
}

func NewXAgent(cookie string) (*XAgent, error) {
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
return nil, err
}

win := new(XAgent)
win.socket = l
cookieWin, err := createDefaultWindow(xAgentClassName, cookie)
if err != nil {
return nil, err
}
siWin, err := createDefaultWindow(xAgentSingleClassName, xAgentSingleWindowName)
if err != nil {
return nil, err
}
SetWindowLong(cookieWin.window, 0xFFFFFFEB, uintptr(l.Addr().(*net.TCPAddr).Port))
win.cookieWin = cookieWin
win.singleInstanceWin = siWin
return win, nil
}

func (s *XAgent) Listener() net.Listener {
return s.socket
}

func (s *XAgent) Close() {
s.cookieWin.Close()
s.singleInstanceWin.Close()
}

func SetWindowLong(hWnd windows.Handle, index, value uintptr) int32 {
ret, _, _ := pSetWindowLong.Call(
uintptr(hWnd),
index,
value,
)
return int32(ret)
}

0 comments on commit 04175e5

Please sign in to comment.